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