@omelhorsite/sdk 0.5.1 → 0.11.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 CHANGED
@@ -1,308 +1,149 @@
1
- # `@omelhorsite/sdk`
1
+ # @omelhorsite/sdk
2
2
 
3
- The TypeScript client for the omelhorsite API.
3
+ [![npm](https://img.shields.io/npm/v/@omelhorsite/sdk)](https://www.npmjs.com/package/@omelhorsite/sdk)
4
4
 
5
- ```ts
6
- import { Oms } from "@omelhorsite/sdk";
7
-
8
- const oms = new Oms({ token: myToken });
9
-
10
- const me = await oms.auth.whoami();
11
- const link = await oms.shortLinks.create({ url: "https://example.com" });
12
- ```
5
+ TypeScript client for the [omelhorsite](https://omelhorsite.pt) API. Works in
6
+ browsers, Bun, Node 18+, React Native and Cloudflare Workers.
13
7
 
14
8
  ```sh
15
9
  bun add @omelhorsite/sdk
16
10
  ```
17
11
 
18
- ## What shapes every signature
19
-
20
- **It runs anywhere `fetch` runs.** Browsers, Bun, Node 18+, React Native and
21
- Cloudflare-Worker-class isolates. Nothing in the package touches `node:*`,
22
- `process`, the filesystem or `console`; `fetch` is injectable.
23
-
24
- **Files are values, never paths.** `Blob`, `Uint8Array` or `ReadableStream`
25
- in; `Blob` out. On React Native a picked `{ uri, name, type }` is accepted as
26
- it is and streamed by the platform.
27
-
28
- **The types are the documentation.** Everything is exported flat from the
29
- package root, and the JSDoc on each method carries the rate limits, quota
30
- units and traps of the endpoint behind it.
31
-
32
- ## Constructing a client
33
-
34
12
  ```ts
35
13
  import { Oms } from "@omelhorsite/sdk";
36
14
 
37
- const oms = new Oms({
38
- token: "...", // string, function, TokenProvider, or omitted
39
- baseUrl: "http://localhost:3000", // defaults to https://backend.omelhorsite.pt
40
- fetch: myFetch, // defaults to globalThis.fetch
41
- headers: { "X-Trace": id }, // merged under per-call headers
42
- timeoutMs: 30_000, // per attempt; 0 disables
43
- retry: { maxAttempts: 3 }, // or false to never retry
44
- clientName: "my-worker/1.0", // becomes X-Oms-Client
45
- });
46
- ```
47
-
48
- Constructing does no I/O. `oms.withToken(other)` returns a copy under a
49
- different identity rather than mutating one.
15
+ const oms = new Oms({ token: "..." });
50
16
 
51
- Omitting the token is legitimate: short links, notepads, chests, IP lookup and
52
- the captcha-gated tools all work anonymously, at a smaller daily quota. A
53
- browser on the API's own site can use the session cookie instead with
54
- `new Oms({ sessionCookie: true })`.
17
+ const me = await oms.account.me();
18
+ const link = await oms.shortLinks.create({ url: "https://example.com" });
19
+ ```
55
20
 
56
- **Injecting `fetch` is the extension point.** A cache, a test double, a proxy:
57
- none of them patch a global.
21
+ ## Client
58
22
 
59
23
  ```ts
60
24
  const oms = new Oms({
61
- token,
62
- fetch: (url, init) => fetch(url, { ...init, cf: { cacheTtl: 60 } }),
25
+ token: "...", // omit for anonymous access
26
+ baseUrl: "http://localhost:3000", // default: https://backend.omelhorsite.pt
27
+ fetch: myFetch, // default: globalThis.fetch
28
+ timeoutMs: 30_000,
29
+ retry: { maxAttempts: 3 }, // or false
63
30
  });
64
31
  ```
65
32
 
66
- An endpoint the SDK has not wrapped yet is still reachable:
33
+ `token` can be a string, a function returning one, or a `TokenProvider` that
34
+ refreshes itself. On a first-party page use `new Oms({ sessionCookie: true })`
35
+ instead.
67
36
 
68
- ```ts
69
- const rows = await oms.http.get<{ id: string }[]>("/some/new/path");
70
- ```
37
+ Some things work without a token (short links, notepads, chests, IP lookup,
38
+ the captcha-gated tools), at a smaller daily quota.
71
39
 
72
- ## Namespaces
40
+ ## Examples
73
41
 
74
- | | |
75
- | --- | --- |
76
- | `oms.auth` | OAuth: device grant, refresh, revoke, `whoami`, `userinfo`. |
77
- | `oms.sessions` `oms.passkeys` | Session sign-in, sign-up, OTP, passkeys. |
78
- | `oms.account` | The signed-in user, their profile, sessions and usage. |
79
- | `oms.storage` | The virtual filesystem: nodes, uploads, downloads, grants. |
80
- | `oms.media` | Resolving stored media to URLs. |
81
- | `oms.music` | Songs, artists, playlists, imports, likes, jams, the social feed. |
82
- | `oms.movies` | Addons, collections, watch progress. |
83
- | `oms.library` | Books, shelves, annotations, the study assistant. |
84
- | `oms.social` | Direct messages, relationships, group chats. |
85
- | `oms.content` | Blogs, notifications, feedbacks, jokes, site status, intel. |
86
- | `oms.tools` | The metered media tools, each with its own daily quota. |
87
- | `oms.jobs` | Background jobs: list, get, wait, watch. |
88
- | `oms.quotas` | Every ceiling on the account in one call. |
89
- | `oms.tickets` | Support tickets and their message threads. |
90
- | `oms.shortLinks` `oms.notepads` `oms.dynamicQrs` `oms.chests` `oms.forms` `oms.linkTrees` | Everything that ends in a shareable URL. |
91
- | `oms.ipLookup` | Geolocation and network metadata for an IP. |
92
- | `oms.admin` | Administrator-only views and actions. |
93
- | `oms.realtime` | The WebSocket channel: notifications, jobs, jams. |
94
- | `oms.local` | Pure client-side helpers. No network, no credential. |
95
-
96
- ## Listing and filtering
97
-
98
- Every `list()` takes the same query language, typed per resource:
42
+ Listing and paging:
99
43
 
100
44
  ```ts
45
+ import { collect } from "@omelhorsite/sdk";
46
+
101
47
  const page = await oms.library.books.list({
102
- search: { title: "maias" }, // partial, accent-insensitive
103
- exactSearch: { format: "epub" }, // equality; an array is IN, null is IS NULL
104
- extraOptions: { scope: "mine" }, // endpoint-specific, only where declared
105
- order: "created_at:desc", // "column:asc" | "column:desc"
106
- page: 2,
107
- pageSize: 50, // capped at 500 by the server
48
+ search: { title: "maias" },
49
+ order: "created_at:desc",
50
+ pageSize: 50,
108
51
  });
109
- ```
110
-
111
- The columns each resource accepts are string-literal unions, so a key the
112
- server would reject with `400` is a compile error instead. Most resources also
113
- offer camelCased shortcuts (`userId`, `withUser`, `ownerHandle`) that write
114
- into the same buckets.
115
-
116
- The result is a `Paginated<T>`: `items`, `page`, `pageSize`, `hasMore` and
117
- `next()`. Two helpers walk it:
118
52
 
119
- ```ts
120
- import { collect, pages } from "@omelhorsite/sdk";
121
-
122
- const all = await collect(page, 5000); // flatten, up to a limit
123
- for await (const p of pages(page)) { ... } // or one page at a time
53
+ page.items; // this page
54
+ await page.next(); // the next one, or null
55
+ await collect(page, 1000); // flatten up to a limit
124
56
  ```
125
57
 
126
- Always pass a limit to `collect`; a listing can be very long.
127
-
128
- ## Files
129
-
130
- A `FileInput` always carries a filename, because the API derives the stored
131
- name and, for the media tools, the container format from it:
58
+ Uploading a file:
132
59
 
133
60
  ```ts
134
61
  import { file } from "@omelhorsite/sdk";
135
62
 
136
- const audio = file(blob, "entrevista.m4a");
137
- const bytes = file(new Uint8Array(buffer), "dump.sql", { contentType: "application/sql" });
138
- const streamed = file(response.body!, "big.mov", { size: contentLength });
139
- ```
140
-
141
- Pass `size` when you know it: it lets `storage.upload` choose the multipart
142
- path (32 MiB and up) without buffering the stream to measure it.
143
-
144
- Downloads come back as a `Blob`, or as a `FileOutput` when the server's
145
- filename and content type matter:
146
-
147
- ```ts
148
- const out = await oms.storage.download(nodeId);
149
- const { stream } = await oms.storage.downloadStream(nodeId);
63
+ const roots = await oms.storage.roots();
64
+ const [node] = await oms.storage.upload({
65
+ parentId: roots.home!,
66
+ files: [file(blob, "relatorio.pdf")],
67
+ });
150
68
  ```
151
69
 
152
- ## Uploading to storage
70
+ Files are values (`Blob`, `Uint8Array`, `ReadableStream`), never paths. On
71
+ React Native pass the picker's `{ uri, name, type }` directly.
153
72
 
154
- Bytes go straight to object storage through a presigned URL and are bound to
155
- the node at the end.
73
+ Running a tool (transcription, upscale, background removal, ...):
156
74
 
157
75
  ```ts
158
- const roots = await oms.storage.roots();
159
-
160
- const nodes = await oms.storage.upload(
161
- { parentId: roots.home!, files: [file(blob, "relatorio.pdf")], concurrency: 4 },
162
- { onProgress: (p) => report(p.loaded, p.total) },
76
+ const result = await oms.tools.transcription.run(
77
+ { audio: file(blob, "entrevista.m4a"), language: "pt" },
78
+ { onProgress: (p) => console.log(p.status) },
163
79
  );
164
80
  ```
165
81
 
166
- In a browser or on React Native, progress is reported byte by byte (the bytes
167
- travel through `XMLHttpRequest`); elsewhere it arrives per finished file or
168
- part. The same goes for every other upload: pass `onUploadProgress` in the
169
- options of `tools.*.create`, `library.books.create` or
170
- `chests.entries.createWithUpload`. A file rejected on its own (quota, name
171
- collision) does not throw; it is missing from the returned array, so compare
172
- lengths when partial success matters.
82
+ `run` starts the job and waits for it. Use `create` + `oms.jobs.wait` if you
83
+ would rather come back to it later.
173
84
 
174
- ## Long jobs: `create` / `get`, and `run`
175
-
176
- Every metered tool is asynchronous: the server enqueues the work and answers
177
- with a row in `"pending"`, plus a `job_id` and, for an anonymous caller, a
178
- `watch_token` scoped to that one job.
85
+ Talking to the assistant:
179
86
 
180
87
  ```ts
181
- const started = await oms.tools.transcription.create({ audio, language: "pt" });
182
- const now = await oms.tools.transcription.get(started.id);
88
+ const chat = await oms.llm.chats.create();
183
89
 
184
- const done = await oms.tools.transcription.run(
185
- { audio, language: "pt" },
186
- { onProgress: (p) => report(p.status), waitTimeoutMs: 15 * 60_000 },
187
- );
90
+ for await (const event of oms.llm.chats.send(chat.id, { content: "Olá!" })) {
91
+ if (event.type === "delta") process.stdout.write(event.delta);
92
+ }
188
93
  ```
189
94
 
190
- `run` is `create` plus `jobs.wait`. Use `create` on hosts with a wall-clock
191
- budget or nowhere to hold a wait: start the job, keep the id, pick it up later.
192
-
193
- Waiting resolves for both `"complete"` and `"failed"`: a failed job is an
194
- answer, not a transport error. Check the status before reading the result.
95
+ An endpoint the SDK does not wrap yet:
195
96
 
196
97
  ```ts
197
- const job = await oms.jobs.wait({ id: started.job_id!, watchToken: started.watch_token });
198
- if (job.status === "failed") throw new Error(job.error ?? "the job failed");
98
+ const rows = await oms.http.get<{ id: string }[]>("/some/path");
199
99
  ```
200
100
 
201
- A finished job says `status: "complete"`, not `"completed"`, and the
202
- downloader spells its own terminal state `"done"`. Compare against the exported
203
- constants, never against a literal.
204
-
205
- Check the quota before starting something expensive:
101
+ ## Namespaces
206
102
 
207
- ```ts
208
- const quota = await oms.tools.transcription.quota();
209
- if (!quota.unlimited && (quota.remaining_seconds ?? 0) < 60) return;
210
- ```
103
+ - `oms.auth` - OAuth: device grant, refresh, revoke, `whoami`.
104
+ - `oms.sessions`, `oms.passkeys` - sign-in, sign-up, OTP, passkeys.
105
+ - `oms.account` - the signed-in user, profile, sessions, usage. `oms.account.notificationPreferences` decides, per notification kind, whether it shows in the inbox and whether it is emailed; the security kinds always email, unless the master switch is off.
106
+ - `oms.storage` - files and folders: upload, download, share.
107
+ - `oms.music` - songs, artists, playlists, jams.
108
+ - `oms.movies` - addons, collections, watch progress.
109
+ - `oms.library` - books, shelves, annotations.
110
+ - `oms.llm` - models and assistant chats.
111
+ - `oms.search` - web, image, news and video search.
112
+ - `oms.social` - direct messages, friends, group chats.
113
+ - `oms.content` - blogs, notifications, feedback, site status. `oms.content.notifications.unsubscribe(token)` honours the link at the foot of a notification email and needs no credential.
114
+ - `oms.tools` - media tools, each with its own daily quota.
115
+ - `oms.jobs`, `oms.quotas` - background jobs and account limits.
116
+ - `oms.tickets` - support tickets.
117
+ - `oms.shortLinks`, `oms.notepads`, `oms.dynamicQrs`, `oms.chests`, `oms.forms`, `oms.linkTrees` - things that end in a shareable URL.
118
+ - `oms.ipLookup` - geolocation for an IP.
119
+ - `oms.realtime` - WebSocket: notifications, jobs, jams.
120
+ - `oms.admin` - administrator-only.
121
+ - `oms.local` - client-side helpers (passwords, QR codes). No network.
211
122
 
212
123
  ## Errors
213
124
 
214
- Every failure is an `OmsError` subclass carrying what is needed to decide
215
- between retrying, re-scoping and giving up.
125
+ Everything thrown is an `OmsError`:
216
126
 
217
127
  ```ts
218
- import { OmsApiError, OmsAuthError, OmsQuotaError, OmsTimeoutError, readInsufficientScope } from "@omelhorsite/sdk";
128
+ import { OmsApiError, OmsAuthError, OmsQuotaError } from "@omelhorsite/sdk";
219
129
 
220
130
  try {
221
131
  await oms.storage.delete(id);
222
- } catch (thrown) {
223
- if (thrown instanceof OmsQuotaError) return retryAfter(thrown.retryAfterMs);
224
-
225
- const missing = readInsufficientScope(thrown);
226
- if (missing) return askForScopes(missing.scope);
227
-
228
- if (thrown instanceof OmsAuthError) return signInAgain();
229
- if (thrown instanceof OmsApiError) log(thrown.status, thrown.fieldErrors);
230
- throw thrown;
132
+ } catch (e) {
133
+ if (e instanceof OmsQuotaError) wait(e.retryAfterMs);
134
+ else if (e instanceof OmsAuthError) signIn();
135
+ else if (e instanceof OmsApiError) console.log(e.status, e.message);
136
+ else throw e;
231
137
  }
232
138
  ```
233
139
 
234
- `OmsNetworkError` means the API was never reached; `OmsTimeoutError` with
235
- `code === "aborted"` means your own `signal` fired. Retries are on by default
236
- for idempotent requests, with backoff and jitter. Pass `retry: false` to any
237
- create you would rather see fail than duplicate.
238
-
239
- ## OAuth
240
-
241
- The device grant, in full. `/oauth/token` authenticates with `client_id` in
242
- the form body; a stray `Authorization` header breaks it, so start from a
243
- client with no token.
244
-
245
- ```ts
246
- import { Oms, OAuthTokenProvider, decodeIdToken } from "@omelhorsite/sdk";
247
-
248
- const anon = new Oms({ baseUrl, fetch });
249
-
250
- const grant = await anon.auth.device.start({ clientId, scope: "openid storage:read" });
251
- show(grant.verificationUriComplete ?? grant.verificationUri, grant.userCode);
252
-
253
- const set = await anon.auth.device.wait({
254
- clientId,
255
- deviceCode: grant.deviceCode,
256
- intervalMs: grant.intervalMs,
257
- expiresAt: grant.expiresAt,
258
- });
259
-
260
- const tokens = new OAuthTokenProvider({
261
- store: myTokenStore,
262
- refresh: (refreshToken) => anon.auth.refresh(refreshToken, { clientId }),
263
- });
264
- await tokens.set(set);
265
-
266
- const oms = new Oms({ baseUrl, fetch, tokens }); // refreshes itself on a 401
267
- ```
268
-
269
- `decodeIdToken(set.idToken)` reads the claims. `sub` is the user id, stable
270
- and the only identifier safe to key on; the handle and the email are mutable.
271
-
272
- Access tokens live two hours. `OMS_SCOPES` is the full list the server
273
- defines; ask for the narrowest set that does the job.
274
-
275
- ## Local helpers
276
-
277
- No network, no credential:
278
-
279
- ```ts
280
- import { generatePassphrase, generatePassword, passwordStrength, qrToSvg } from "@omelhorsite/sdk";
281
-
282
- const phrase = generatePassphrase({ words: 5, capitalize: true });
283
- const score = passwordStrength(generatePassword({ length: 20 }));
284
- const svg = qrToSvg("https://example.com");
285
- ```
286
-
287
- ## Testing against it
288
-
289
- Inject a fetch. There is no global to stub:
290
-
291
- ```ts
292
- const oms = new Oms({
293
- token: "test",
294
- fetch: async () => new Response(JSON.stringify({ id: "1" }), {
295
- status: 200,
296
- headers: { "content-type": "application/json" },
297
- }),
298
- });
299
- ```
140
+ `OmsNetworkError` means the API was never reached, `OmsTimeoutError` that the
141
+ request took too long.
300
142
 
301
143
  ## Developing
302
144
 
303
145
  ```sh
304
146
  bun test
305
147
  bun run typecheck
306
- bun run check:isolate
307
148
  bun run build
308
149
  ```