@onlyworlds/sdk 3.1.0 → 4.0.0-alpha.1
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/AGENTS.md +34 -30
- package/CHANGELOG.md +103 -44
- package/README.md +144 -332
- package/SCHEMA.md +630 -0
- package/dist/index.d.ts +475 -1471
- package/dist/index.js +713 -1001
- package/package.json +61 -53
- package/dist/index.d.mts +0 -3517
- package/dist/index.mjs +0 -1576
package/README.md
CHANGED
|
@@ -1,332 +1,144 @@
|
|
|
1
|
-
# OnlyWorlds TypeScript SDK
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@onlyworlds/sdk)
|
|
4
|
-
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// Get your world
|
|
146
|
-
const world = await client.worlds.get();
|
|
147
|
-
|
|
148
|
-
// Update your world
|
|
149
|
-
const updated = await client.worlds.update({
|
|
150
|
-
description: 'A dark fantasy realm'
|
|
151
|
-
});
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
**Note**: The `worlds` resource only has `get()` and `update()` methods (no `list()`, `create()`, or `delete()`) because API keys are world-scoped.
|
|
155
|
-
|
|
156
|
-
### Element Endpoints
|
|
157
|
-
|
|
158
|
-
All other element types return paginated results:
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
// List with pagination
|
|
162
|
-
const response = await client.characters.list({
|
|
163
|
-
limit: 10,
|
|
164
|
-
offset: 0,
|
|
165
|
-
ordering: '-created_at',
|
|
166
|
-
search: 'dragon'
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
console.log(response.count); // Total count
|
|
170
|
-
console.log(response.results); // Array of Characters
|
|
171
|
-
console.log(response.next); // URL for next page
|
|
172
|
-
console.log(response.previous); // URL for previous page
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
## Type-Safe Relationships
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
import { ElementId, Character, Location } from '@onlyworlds/sdk';
|
|
179
|
-
|
|
180
|
-
// Branded types ensure you can't mix up element IDs
|
|
181
|
-
const locationId: ElementId<'Location'> = 'some-location-id';
|
|
182
|
-
const character: Character = {
|
|
183
|
-
name: 'Aragorn',
|
|
184
|
-
location: locationId // Type-safe!
|
|
185
|
-
};
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## Token Management
|
|
189
|
-
|
|
190
|
-
OnlyWorlds provides a token rating system for tracking API usage and enabling AI-powered features. Users get a daily token allowance (default: 10,000 tokens) that resets every day.
|
|
191
|
-
|
|
192
|
-
### Working Reference Implementation
|
|
193
|
-
|
|
194
|
-
See [base-tool/src/llm/token-service.ts](https://github.com/OnlyWorlds/base-tool) for the complete working implementation that this SDK enables.
|
|
195
|
-
|
|
196
|
-
### Check Token Status
|
|
197
|
-
|
|
198
|
-
```typescript
|
|
199
|
-
// Get current token status
|
|
200
|
-
const status = await client.tokens.getStatus();
|
|
201
|
-
|
|
202
|
-
console.log(`Available: ${status.tokens_available_today}/${status.token_rating}`);
|
|
203
|
-
console.log(`Used today: ${status.tokens_used_today}`);
|
|
204
|
-
console.log(`Active sessions: ${status.sessions_active}`);
|
|
205
|
-
console.log(`Last reset: ${status.last_reset}`);
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
### Consume Tokens
|
|
209
|
-
|
|
210
|
-
Report token consumption when your tool uses AI features or other token-tracked services:
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
// Report token usage
|
|
214
|
-
const result = await client.tokens.consume({
|
|
215
|
-
amount: 500,
|
|
216
|
-
service: 'my_worldbuilding_tool',
|
|
217
|
-
metadata: {
|
|
218
|
-
feature: 'character_generation',
|
|
219
|
-
model: 'gpt-4',
|
|
220
|
-
prompt_tokens: 300,
|
|
221
|
-
completion_tokens: 200
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// Check if consumption succeeded
|
|
226
|
-
if (result.error) {
|
|
227
|
-
console.warn('Token warning:', result.error);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
console.log(`Consumed ${result.tokens_consumed} tokens`);
|
|
231
|
-
console.log(`${result.tokens_remaining} tokens remaining`);
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
**Important**: The API allows consumption even when exceeding available tokens (tracks as debt), but returns a warning in the `error` field.
|
|
235
|
-
|
|
236
|
-
### Advanced: Encrypted API Key Access
|
|
237
|
-
|
|
238
|
-
For tools that need direct OpenAI API access using OnlyWorlds tokens:
|
|
239
|
-
|
|
240
|
-
```typescript
|
|
241
|
-
// Get encrypted OpenAI key (requires 100+ tokens)
|
|
242
|
-
const access = await client.tokens.getAccessKey();
|
|
243
|
-
|
|
244
|
-
console.log('Session ID:', access.session_id);
|
|
245
|
-
console.log('Expires:', access.expires_at);
|
|
246
|
-
console.log('Available tokens:', access.tokens_available);
|
|
247
|
-
|
|
248
|
-
// Decrypt the key client-side (see base-tool for full implementation)
|
|
249
|
-
// 1. Derive decryption key from world ID using SHA-256
|
|
250
|
-
// 2. Use 'fernet' npm package to decrypt
|
|
251
|
-
// 3. Use decrypted OpenAI key for direct API calls
|
|
252
|
-
// 4. Report usage with session_id
|
|
253
|
-
|
|
254
|
-
// See base-tool/src/llm/token-service.ts:99-235 for complete example
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
**Full decryption implementation** (based on base-tool):
|
|
258
|
-
|
|
259
|
-
```typescript
|
|
260
|
-
import { fernet } from 'fernet';
|
|
261
|
-
|
|
262
|
-
// Derive decryption key from world ID
|
|
263
|
-
async function deriveKey(worldId: string): Promise<string> {
|
|
264
|
-
const salt = 'onlyworlds-token-api-2024-public-salt';
|
|
265
|
-
const keyMaterial = `${worldId}:${salt}`;
|
|
266
|
-
const encoder = new TextEncoder();
|
|
267
|
-
const data = encoder.encode(keyMaterial);
|
|
268
|
-
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
269
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
270
|
-
const base64 = btoa(String.fromCharCode(...hashArray));
|
|
271
|
-
return base64.replace(/\+/g, '-').replace(/\//g, '_');
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Decrypt the API key
|
|
275
|
-
async function decryptApiKey(encryptedKey: string, worldId: string): Promise<string> {
|
|
276
|
-
const derivedKey = await deriveKey(worldId);
|
|
277
|
-
const secret = new fernet.Secret(derivedKey);
|
|
278
|
-
const token = new fernet.Token({
|
|
279
|
-
secret: secret,
|
|
280
|
-
token: encryptedKey,
|
|
281
|
-
ttl: 0 // Don't enforce TTL client-side
|
|
282
|
-
});
|
|
283
|
-
return token.decode();
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Usage
|
|
287
|
-
const world = await client.worlds.get();
|
|
288
|
-
const access = await client.tokens.getAccessKey();
|
|
289
|
-
const apiKey = await decryptApiKey(access.encrypted_key, world.id);
|
|
290
|
-
|
|
291
|
-
// Use apiKey for OpenAI API calls, then report usage:
|
|
292
|
-
await client.tokens.consume({
|
|
293
|
-
amount: tokensUsed,
|
|
294
|
-
sessionId: access.session_id,
|
|
295
|
-
service: 'direct_openai',
|
|
296
|
-
metadata: { model: 'gpt-4', /* ... */ }
|
|
297
|
-
});
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
### Session Management
|
|
301
|
-
|
|
302
|
-
```typescript
|
|
303
|
-
// Revoke a specific session
|
|
304
|
-
await client.tokens.revokeSession(sessionId);
|
|
305
|
-
|
|
306
|
-
// Revoke all sessions (emergency cleanup)
|
|
307
|
-
const result = await client.tokens.revokeAllSessions();
|
|
308
|
-
console.log(`Revoked ${result.sessions_revoked} sessions`);
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
### Encryption Info
|
|
312
|
-
|
|
313
|
-
Get public encryption details (no auth required):
|
|
314
|
-
|
|
315
|
-
```typescript
|
|
316
|
-
const info = await client.tokens.getEncryptionInfo();
|
|
317
|
-
console.log('Algorithm:', info.algorithm);
|
|
318
|
-
console.log('Key derivation:', info.key_derivation);
|
|
319
|
-
console.log('Public salt:', info.salt);
|
|
320
|
-
console.log(info.javascript_example);
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
## License
|
|
324
|
-
|
|
325
|
-
MIT License - see [LICENSE](LICENSE) file for details
|
|
326
|
-
|
|
327
|
-
## Links
|
|
328
|
-
|
|
329
|
-
- [OnlyWorlds Website](https://onlyworlds.com)
|
|
330
|
-
- [Documentation](https://onlyworlds.github.io/)
|
|
331
|
-
- [NPM Package](https://www.npmjs.com/package/@onlyworlds/sdk)
|
|
332
|
-
- [Report Issues](https://github.com/OnlyWorlds/sdk/issues)
|
|
1
|
+
# OnlyWorlds TypeScript SDK
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@onlyworlds/sdk)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
The canonical typed client for the [OnlyWorlds](https://onlyworlds.github.io) v2 API, plus the
|
|
7
|
+
canonical constants (element types, icons, colour families, field schema) — generated from the
|
|
8
|
+
same schema source the server runs on.
|
|
9
|
+
|
|
10
|
+
**4.x is v2-native and ESM-only (Node 18+).** If you need the legacy v1 API dialect
|
|
11
|
+
(`OnlyWorldsClient`) or CommonJS `require()`, stay on 3.x — it remains published and the v1 API
|
|
12
|
+
remains served. See [docs/migrating-3-to-4.md](docs/migrating-3-to-4.md).
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @onlyworlds/sdk
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { OwV2Client } from '@onlyworlds/sdk';
|
|
24
|
+
|
|
25
|
+
const client = new OwV2Client({ apiKey: 'ow_r_your_key' }); // read-only key: that's all you need
|
|
26
|
+
const page = await client.list('character'); // { data, has_more, next_cursor }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Key kinds: `ow_w_` write / `ow_r_` read-only (no PIN — the "give this to your players" key) /
|
|
30
|
+
`ow_a_` account Bearer / 10-digit legacy. Writes on a PIN-protected world also pass `apiPin`.
|
|
31
|
+
Demo keys `0000000000`–`0000000009` read real sample worlds.
|
|
32
|
+
|
|
33
|
+
## The full round-trip
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
const writer = new OwV2Client({ apiKey: 'ow_w_your_key', apiPin: '1234' });
|
|
37
|
+
|
|
38
|
+
// v2 dialect rules, worth internalizing once:
|
|
39
|
+
// - link fields use ONE bare name both directions (no _ids suffix — that's v1)
|
|
40
|
+
// - NEVER send a "world" field (identity comes from the key; the client strips it anyway)
|
|
41
|
+
const location = await writer.create('location', {
|
|
42
|
+
name: 'Dragon Peak',
|
|
43
|
+
description: 'A treacherous mountain peak where dragons nest',
|
|
44
|
+
}); // id minted client-side when omitted (retries stay idempotent)
|
|
45
|
+
|
|
46
|
+
const dragon = await writer.create('creature', {
|
|
47
|
+
name: 'Vorrath the Ember-Scaled',
|
|
48
|
+
location: location.id, // single link: UUID (or null)
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const fetched = await writer.get('creature', dragon.id);
|
|
52
|
+
// fetched.location === location.id — reads the way it writes
|
|
53
|
+
|
|
54
|
+
// PATCH is destructive on sent fields; arrays replace wholesale
|
|
55
|
+
await writer.patch('location', location.id, { supertype: 'Mountain' });
|
|
56
|
+
|
|
57
|
+
// For relationships, use the atomic link merge — returns the full updated element
|
|
58
|
+
const fireBreath = await writer.create('ability', { name: 'Ember Breath' });
|
|
59
|
+
await writer.editLinks('creature', dragon.id, 'abilities', { add: [fireBreath.id], remove: [] });
|
|
60
|
+
|
|
61
|
+
// Walk every page of a type
|
|
62
|
+
for await (const character of writer.listAll('character')) { /* ... */ }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Bulk writes — always check `errors`
|
|
66
|
+
|
|
67
|
+
`/bulk` **succeeds partially by default** (HTTP 200 with per-slot statuses). The one mistake to
|
|
68
|
+
never make: treating a 200 as "everything landed."
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
const res = await writer.bulk(
|
|
72
|
+
[
|
|
73
|
+
{ type: 'character', element: { name: 'A' } },
|
|
74
|
+
{ type: 'event', element: { name: 'B' } },
|
|
75
|
+
],
|
|
76
|
+
{ idempotencyKey: crypto.randomUUID() }, // mint FRESH per attempt — a failed batch is
|
|
77
|
+
); // cached under its key; never reuse across retries
|
|
78
|
+
if (res.errors) {
|
|
79
|
+
for (const slot of res.items.filter((s) => s.status >= 400)) {
|
|
80
|
+
console.warn(slot.error?.code, slot.error?.message, slot.error?.doc_url);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Pass { atomic: true } for all-or-nothing instead. res.wasReplay flags idempotent replays.
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Sync
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
let cursor; // opaque, never expires; persist it
|
|
90
|
+
for await (const change of client.changesAll(cursor)) {
|
|
91
|
+
// ops arrive in (change_seq, id) order — apply in order → convergence
|
|
92
|
+
}
|
|
93
|
+
// GOTCHA: world-meta edits (name, calendar, public_read) do NOT enter /changes.
|
|
94
|
+
// Poll client.getWorld() and compare updated_at separately.
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Errors
|
|
98
|
+
|
|
99
|
+
Every non-2xx throws `OwApiError` carrying the platform envelope: `.status`, `.type`, `.code`,
|
|
100
|
+
`.param` (the exact field that failed), and `.docUrl` — surface `docUrl` in your UX. Transport
|
|
101
|
+
failures throw `OwNetworkError`. `err.isValidationError` is the common branch.
|
|
102
|
+
|
|
103
|
+
## Canonical constants (all generated or test-gated from schema)
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import {
|
|
107
|
+
ELEMENT_TYPES, // the 22 slugs (and the ElementType union)
|
|
108
|
+
ELEMENT_ICONS, // Material Symbols icon per type
|
|
109
|
+
ELEMENT_LABELS, // plural display labels
|
|
110
|
+
ELEMENT_SECTIONS, // canonical field grouping + display order
|
|
111
|
+
FIELD_SCHEMA, // per-field type/target metadata
|
|
112
|
+
elementColor, // canonical colour: family carries COLOUR, icon carries TYPE
|
|
113
|
+
} from '@onlyworlds/sdk';
|
|
114
|
+
|
|
115
|
+
elementColor('character', 'dark'); // '#3987e5' — four CVD-validated families
|
|
116
|
+
// Always pair colour with icon + label; colour alone is not accessible.
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The full schema with per-field meaning lives in [SCHEMA.md](SCHEMA.md) (generated, ships in this
|
|
120
|
+
package). AI agents: read [AGENTS.md](AGENTS.md) first.
|
|
121
|
+
|
|
122
|
+
## Token rating system
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { TokenResource } from '@onlyworlds/sdk';
|
|
126
|
+
const tokens = new TokenResource(writer); // OwV2Client.request() satisfies TokenTransport
|
|
127
|
+
const status = await tokens.getStatus();
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## AI access — when to use which
|
|
131
|
+
|
|
132
|
+
For **known, deterministic operations** (CRUD, sync, bulk) use this SDK — typed calls, no
|
|
133
|
+
tool-schema overhead. For **live exploration of a user's world from a chat/agent context**, use
|
|
134
|
+
the MCP server at `https://www.onlyworlds.com/mcp` (same `API-Key`/`API-Pin` headers, 11 tools;
|
|
135
|
+
unaffected by SDK versioning).
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
|
140
|
+
|
|
141
|
+
## Links
|
|
142
|
+
|
|
143
|
+
- [OnlyWorlds](https://www.onlyworlds.com) · [Docs](https://onlyworlds.github.io) · [API reference](https://www.onlyworlds.com/api/docs)
|
|
144
|
+
- [Issues](https://github.com/OnlyWorlds/sdk/issues) · [CHANGELOG](CHANGELOG.md) · [Migration 3→4](docs/migrating-3-to-4.md)
|