@omelhorsite/sdk 0.1.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 (36) hide show
  1. package/README.md +321 -0
  2. package/dist/index.js +11589 -0
  3. package/dist/types/auth/device.d.ts +156 -0
  4. package/dist/types/auth/index.d.ts +127 -0
  5. package/dist/types/auth/tokens.d.ts +356 -0
  6. package/dist/types/client.d.ts +133 -0
  7. package/dist/types/errors.d.ts +202 -0
  8. package/dist/types/http.d.ts +204 -0
  9. package/dist/types/index.d.ts +33 -0
  10. package/dist/types/local/index.d.ts +42 -0
  11. package/dist/types/local/password.d.ts +169 -0
  12. package/dist/types/local/qr.d.ts +127 -0
  13. package/dist/types/local/wordlist.d.ts +26 -0
  14. package/dist/types/resources/account.d.ts +296 -0
  15. package/dist/types/resources/chests.d.ts +194 -0
  16. package/dist/types/resources/dynamicQrs.d.ts +172 -0
  17. package/dist/types/resources/forms.d.ts +331 -0
  18. package/dist/types/resources/index.d.ts +30 -0
  19. package/dist/types/resources/ipLookup.d.ts +63 -0
  20. package/dist/types/resources/jobs.d.ts +233 -0
  21. package/dist/types/resources/linkTrees.d.ts +249 -0
  22. package/dist/types/resources/notepads.d.ts +96 -0
  23. package/dist/types/resources/shortLinks.d.ts +248 -0
  24. package/dist/types/resources/storage/upload.d.ts +459 -0
  25. package/dist/types/resources/storage.d.ts +527 -0
  26. package/dist/types/resources/tickets.d.ts +236 -0
  27. package/dist/types/resources/tools/backgroundRemoval.d.ts +99 -0
  28. package/dist/types/resources/tools/captions.d.ts +318 -0
  29. package/dist/types/resources/tools/downloader.d.ts +397 -0
  30. package/dist/types/resources/tools/index.d.ts +215 -0
  31. package/dist/types/resources/tools/jumpstyle.d.ts +194 -0
  32. package/dist/types/resources/tools/transcription.d.ts +178 -0
  33. package/dist/types/resources/tools/upscale.d.ts +94 -0
  34. package/dist/types/resources/tools/vocalSeparation.d.ts +183 -0
  35. package/dist/types/types.d.ts +245 -0
  36. package/package.json +37 -0
package/README.md ADDED
@@ -0,0 +1,321 @@
1
+ # `@omelhorsite/sdk`
2
+
3
+ The TypeScript client for the omelhorsite API. The CLI and the MCP server are
4
+ clients of this package, not the other way round.
5
+
6
+ ```ts
7
+ import { Oms } from "@omelhorsite/sdk";
8
+
9
+ const oms = new Oms({ token: myToken });
10
+
11
+ const me = await oms.auth.whoami();
12
+ const link = await oms.shortLinks.create({ url: "https://example.com" });
13
+ ```
14
+
15
+ ## Three rules that shape every signature
16
+
17
+ **1. It runs in a Cloudflare-Worker-class isolate.** No `node:*`, no `process`,
18
+ no filesystem, no `console`. Only platform APIs: `fetch`, Web Streams,
19
+ WebCrypto, `Blob`, `FormData`, `AbortController`. This is enforced at compile
20
+ time - `src/isolate-guard.d.ts` declares a poisoned `process`, so reaching for
21
+ one is a type error, not a runtime surprise in production.
22
+
23
+ **2. Files are values, never paths.** `Blob`, `Uint8Array`, `ReadableStream` in;
24
+ `Blob` out. Turning a path into bytes is the host's job, because the isolate has
25
+ no path to turn.
26
+
27
+ **3. The types are the public interface.** Everything is re-exported flat from
28
+ the package root; there is no deep import into `src/`. In code mode a model
29
+ reads the `.d.ts` and nothing else, so the JSDoc carries the rate limits, the
30
+ quota units and the places the backend surprises you.
31
+
32
+ ## Installing
33
+
34
+ Today it is a workspace dependency:
35
+
36
+ ```json
37
+ { "dependencies": { "@omelhorsite/sdk": "workspace:*" } }
38
+ ```
39
+
40
+ It ships as TypeScript source, no build step. Bun and any bundler resolve it
41
+ directly. Nothing is published to a registry yet.
42
+
43
+ ## Constructing a client
44
+
45
+ ```ts
46
+ import { Oms } from "@omelhorsite/sdk";
47
+
48
+ const oms = new Oms({
49
+ token: "...", // string, function, TokenProvider, or omitted
50
+ baseUrl: "http://localhost:3000", // defaults to https://backend.omelhorsite.pt
51
+ fetch: myFetch, // defaults to globalThis.fetch
52
+ headers: { "X-Trace": id }, // merged under per-call headers
53
+ timeoutMs: 30_000, // whole call, retries included; 0 disables
54
+ retry: { maxAttempts: 3 }, // or false to never retry
55
+ clientName: "my-worker/1.0", // becomes X-Oms-Client
56
+ });
57
+ ```
58
+
59
+ Constructing does no I/O. `oms.withToken(other)` returns a copy under a
60
+ different identity rather than mutating one, so an in-flight request can never
61
+ finish under the wrong credential.
62
+
63
+ Omitting the token is legitimate: short links, notepads, chests, IP lookup and
64
+ the captcha-gated tools all work anonymously, at a smaller daily quota.
65
+
66
+ **Injecting `fetch` is the extension point.** A Worker that wants a cache, a
67
+ test that wants a double, a host that wants a proxy - none of them patch a
68
+ global:
69
+
70
+ ```ts
71
+ const oms = new Oms({
72
+ token,
73
+ fetch: (url, init) => fetch(url, { ...init, cf: { cacheTtl: 60 } }),
74
+ });
75
+ ```
76
+
77
+ An endpoint the SDK has not wrapped yet is still reachable, which beats forking
78
+ the package to add one call:
79
+
80
+ ```ts
81
+ const rows = await oms.http.get<{ id: string }[]>("/some/new/path");
82
+ ```
83
+
84
+ ## Namespaces
85
+
86
+ | | |
87
+ | --- | --- |
88
+ | `oms.auth` | Device grant, refresh, revoke, `whoami`, `userinfo`. |
89
+ | `oms.account` | The signed-in user, their profile, their usage report. |
90
+ | `oms.storage` | The virtual filesystem: nodes, uploads, downloads, grants. |
91
+ | `oms.tools` | The metered media tools, each with its own daily quota. |
92
+ | `oms.jobs` | Background jobs: list, get, wait, watch. The API has no cancel. |
93
+ | `oms.tickets` | Support tickets and their message threads. |
94
+ | `oms.shortLinks` `oms.notepads` `oms.dynamicQrs` `oms.chests` `oms.forms` `oms.linkTrees` | Everything that ends in a shareable URL. |
95
+ | `oms.ipLookup` | Geolocation and network metadata for an IP. |
96
+ | `oms.local` | Pure client-side helpers. No network, no credential. |
97
+
98
+ ## Files
99
+
100
+ A `FileInput` always carries a filename, because the API derives the stored name
101
+ and, for the media tools, the container format from it. The `file()` helper
102
+ exists so that requirement stays visible at the call site:
103
+
104
+ ```ts
105
+ import { file } from "@omelhorsite/sdk";
106
+
107
+ const audio = file(blob, "entrevista.m4a");
108
+ const bytes = file(new Uint8Array(buffer), "dump.sql", { contentType: "application/sql" });
109
+ const streamed = file(response.body!, "big.mov", { size: contentLength });
110
+ ```
111
+
112
+ Pass `size` when you know it: it lets `storage.upload` pick the multipart path
113
+ (anything from 32 MiB up) without buffering the stream to measure it. A stream
114
+ without a size gets buffered, which for a 2 GB file is not what you want.
115
+
116
+ Downloads come back as a `Blob`, or as a `FileOutput` when the server's
117
+ filename and content type matter:
118
+
119
+ ```ts
120
+ const out = await oms.storage.download(nodeId); // { data, filename, contentType, size }
121
+ const { stream } = await oms.storage.downloadStream(nodeId); // for large files
122
+ ```
123
+
124
+ ## Uploading to storage
125
+
126
+ Bytes never pass through Rails. `upload` mints a plan, sends the bytes straight
127
+ to object storage with a presigned URL, and binds the blob at the end.
128
+
129
+ ```ts
130
+ const roots = await oms.storage.roots();
131
+
132
+ const nodes = await oms.storage.upload(
133
+ { parentId: roots.home!, files: [file(blob, "relatorio.pdf")], concurrency: 4 },
134
+ { onProgress: (p) => report(p.loaded, p.total) },
135
+ );
136
+ ```
137
+
138
+ Progress arrives per finished file or part, never per byte: `fetch` has no
139
+ upload-progress event, and faking one would be a lie. A file rejected on its own
140
+ (quota, name collision) does not throw - it is simply missing from the returned
141
+ array, so compare lengths when partial success matters.
142
+
143
+ ## Pagination
144
+
145
+ Every listing returns a `Paginated<T>` with a `load` function, and two helpers
146
+ consume it:
147
+
148
+ ```ts
149
+ import { collect, pages } from "@omelhorsite/sdk";
150
+
151
+ const first = await oms.storage.list({ parentId, pageSize: 500 });
152
+
153
+ const all = await collect(first, 5000); // flatten, up to a limit
154
+ for await (const page of pages(first)) { ... } // or one page at a time
155
+ ```
156
+
157
+ Always pass a limit to `collect`. A directory with 300k nodes is a real thing
158
+ that has happened here.
159
+
160
+ ## Long jobs: `create` / `get`, and `run`
161
+
162
+ Every metered tool is asynchronous. The server enqueues work and answers
163
+ immediately with a row in `"pending"`, plus a `job_id` and - for an anonymous
164
+ caller - a `watch_token` scoped to that one job.
165
+
166
+ So every tool namespace has the same three-part shape:
167
+
168
+ ```ts
169
+ // start: returns as soon as the work is enqueued
170
+ const started = await oms.tools.transcription.create({ audio, language: "pt" });
171
+
172
+ // poll: one request, no waiting
173
+ const now = await oms.tools.transcription.get(started.id);
174
+
175
+ // or let the SDK poll for you
176
+ const done = await oms.tools.transcription.run(
177
+ { audio, language: "pt" },
178
+ { onProgress: (p) => report(p.status), waitTimeoutMs: 15 * 60_000 },
179
+ );
180
+ ```
181
+
182
+ `run` is `create` plus `jobs.wait`, and it is the right call in a script or at a
183
+ terminal. **The split exists because in half the places this SDK is meant to
184
+ run, `run` is unusable:**
185
+
186
+ - **A Worker has a wall-clock budget.** Holding a poll loop open for a
187
+ five-minute transcription burns the invocation and then dies without the
188
+ result. Start the job, return the id, and pick it up on the next request.
189
+ - **A request/response host has nowhere to put the wait.** An HTTP handler, an
190
+ MCP tool call and a queue consumer all want to hand back an id now and answer
191
+ later. That is what `oms tools ... --no-wait` and `oms tools status` are built
192
+ on.
193
+ - **The polling policy belongs in one place.** `jobs.wait` starts at
194
+ `pollIntervalMs`, backs off towards a ceiling, honours the caller's `signal`,
195
+ and gives up at `waitTimeoutMs`. No tool module opens a second loop.
196
+
197
+ Waiting resolves for **both** `"completed"` and `"failed"`: a failed job is an
198
+ answer, not a transport error. Check the status before reading the result.
199
+
200
+ ```ts
201
+ const job = await oms.jobs.wait({ id: started.job_id!, watchToken: started.watch_token });
202
+ if (job.status === "failed") throw new Error(job.error ?? "the job failed");
203
+ ```
204
+
205
+ **A trap worth naming once.** A finished tool row says `status: "complete"`. A
206
+ finished row in the generic job table says `"completed"`. The downloader's
207
+ sidecar says `"done"`. Three spellings of one idea, in three different tables.
208
+ Compare against the constants, never against a literal you typed from memory.
209
+
210
+ Check the quota before starting something expensive. The unit differs per tool -
211
+ seconds of media for the audio and video tools, edits for jumpstyle:
212
+
213
+ ```ts
214
+ const quota = await oms.tools.transcription.quota();
215
+ if (!quota.unlimited && (quota.remaining_seconds ?? 0) < 60) return;
216
+ ```
217
+
218
+ ## Errors
219
+
220
+ Every failure is an `OmsError` subclass carrying the context needed to decide
221
+ between retrying, re-scoping and giving up.
222
+
223
+ ```ts
224
+ import { OmsApiError, OmsAuthError, OmsQuotaError, OmsTimeoutError, readInsufficientScope } from "@omelhorsite/sdk";
225
+
226
+ try {
227
+ await oms.storage.delete(id);
228
+ } catch (thrown) {
229
+ if (thrown instanceof OmsQuotaError) return retryAfter(thrown.retryAfterMs);
230
+
231
+ const missing = readInsufficientScope(thrown);
232
+ if (missing) return askForScopes(missing.scope); // 403 with a scope requirement
233
+
234
+ if (thrown instanceof OmsAuthError) return signInAgain();
235
+ if (thrown instanceof OmsApiError) log(thrown.status, thrown.fieldErrors);
236
+ throw thrown;
237
+ }
238
+ ```
239
+
240
+ `OmsNetworkError` means the API was never reached; `OmsTimeoutError` with
241
+ `code === "aborted"` means your own `signal` fired. Retries are on by default
242
+ for idempotent requests with backoff and jitter. **Pass `retry: false` to any
243
+ create you would rather see fail than duplicate** - a replayed short link mints
244
+ a second one under a different endpoint.
245
+
246
+ ## Auth
247
+
248
+ The device grant, in full. Note which calls carry a credential and which must
249
+ not: `/oauth/token` authenticates with `client_id` in the form body and a
250
+ stray `Authorization` header breaks it.
251
+
252
+ ```ts
253
+ import { Oms, OAuthTokenProvider, decodeIdToken } from "@omelhorsite/sdk";
254
+
255
+ const anon = new Oms({ baseUrl, fetch }); // NO token
256
+
257
+ const grant = await anon.auth.device.start({ clientId: "oms-cli", scope: "openid storage:read" });
258
+ show(grant.verificationUriComplete ?? grant.verificationUri, grant.userCode);
259
+
260
+ const set = await anon.auth.device.wait({
261
+ clientId: "oms-cli",
262
+ deviceCode: grant.deviceCode,
263
+ intervalMs: grant.intervalMs,
264
+ expiresAt: grant.expiresAt,
265
+ });
266
+
267
+ const tokens = new OAuthTokenProvider({
268
+ store: myTokenStore, // yours: the SDK writes no files
269
+ refresh: (refreshToken) => anon.auth.refresh(refreshToken, { clientId: "oms-cli" }),
270
+ });
271
+ await tokens.set(set);
272
+
273
+ const oms = new Oms({ baseUrl, fetch, tokens }); // refreshes itself on a 401
274
+ ```
275
+
276
+ `decodeIdToken(set.idToken)` reads the claims. **`sub` is `users.id`** - stable,
277
+ and the only identifier safe to key on. The handle and the email are mutable.
278
+
279
+ Access tokens live two hours. `OMS_SCOPES` is the full list the server defines;
280
+ ask for the narrowest set that does the job.
281
+
282
+ ## Local helpers
283
+
284
+ No network, no credential, importable on their own:
285
+
286
+ ```ts
287
+ import { generatePassphrase, generatePassword, passwordStrength, qrToSvg } from "@omelhorsite/sdk";
288
+
289
+ const phrase = generatePassphrase({ words: 5, capitalize: true });
290
+ const score = passwordStrength(generatePassword({ length: 20 }));
291
+ const svg = qrToSvg("https://example.com");
292
+ ```
293
+
294
+ ## Testing against it
295
+
296
+ Inject a fetch. There is no global to stub and no network to mock at a lower
297
+ level:
298
+
299
+ ```ts
300
+ const oms = new Oms({
301
+ token: "test",
302
+ fetch: async (url, init) => new Response(JSON.stringify({ id: "1" }), {
303
+ status: 200,
304
+ headers: { "content-type": "application/json" },
305
+ }),
306
+ });
307
+ ```
308
+
309
+ ```sh
310
+ bun test
311
+ bun run typecheck
312
+ ```
313
+
314
+ **After any change to `src/`, regenerate the MCP server's type catalogue:**
315
+
316
+ ```sh
317
+ bun run --filter '@omelhorsite/mcp' build:types
318
+ ```
319
+
320
+ The MCP server serves these declarations to models as its entire interface. A
321
+ stale catalogue means the model is reading a signature that no longer exists.