@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.
- package/README.md +321 -0
- package/dist/index.js +11589 -0
- package/dist/types/auth/device.d.ts +156 -0
- package/dist/types/auth/index.d.ts +127 -0
- package/dist/types/auth/tokens.d.ts +356 -0
- package/dist/types/client.d.ts +133 -0
- package/dist/types/errors.d.ts +202 -0
- package/dist/types/http.d.ts +204 -0
- package/dist/types/index.d.ts +33 -0
- package/dist/types/local/index.d.ts +42 -0
- package/dist/types/local/password.d.ts +169 -0
- package/dist/types/local/qr.d.ts +127 -0
- package/dist/types/local/wordlist.d.ts +26 -0
- package/dist/types/resources/account.d.ts +296 -0
- package/dist/types/resources/chests.d.ts +194 -0
- package/dist/types/resources/dynamicQrs.d.ts +172 -0
- package/dist/types/resources/forms.d.ts +331 -0
- package/dist/types/resources/index.d.ts +30 -0
- package/dist/types/resources/ipLookup.d.ts +63 -0
- package/dist/types/resources/jobs.d.ts +233 -0
- package/dist/types/resources/linkTrees.d.ts +249 -0
- package/dist/types/resources/notepads.d.ts +96 -0
- package/dist/types/resources/shortLinks.d.ts +248 -0
- package/dist/types/resources/storage/upload.d.ts +459 -0
- package/dist/types/resources/storage.d.ts +527 -0
- package/dist/types/resources/tickets.d.ts +236 -0
- package/dist/types/resources/tools/backgroundRemoval.d.ts +99 -0
- package/dist/types/resources/tools/captions.d.ts +318 -0
- package/dist/types/resources/tools/downloader.d.ts +397 -0
- package/dist/types/resources/tools/index.d.ts +215 -0
- package/dist/types/resources/tools/jumpstyle.d.ts +194 -0
- package/dist/types/resources/tools/transcription.d.ts +178 -0
- package/dist/types/resources/tools/upscale.d.ts +94 -0
- package/dist/types/resources/tools/vocalSeparation.d.ts +183 -0
- package/dist/types/types.d.ts +245 -0
- 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.
|