@omelhorsite/sdk 0.2.0 → 0.3.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 (44) hide show
  1. package/dist/index.js +4939 -552
  2. package/dist/types/client.d.ts +60 -3
  3. package/dist/types/http.d.ts +444 -19
  4. package/dist/types/index.d.ts +4 -1
  5. package/dist/types/resources/account.d.ts +66 -3
  6. package/dist/types/resources/admin.d.ts +1837 -0
  7. package/dist/types/resources/auth/index.d.ts +39 -0
  8. package/dist/types/resources/auth/passkeys.d.ts +652 -0
  9. package/dist/types/resources/auth/sessions.d.ts +847 -0
  10. package/dist/types/resources/chests.d.ts +54 -3
  11. package/dist/types/resources/content.d.ts +2970 -0
  12. package/dist/types/resources/dynamicQrs.d.ts +39 -3
  13. package/dist/types/resources/forms.d.ts +176 -35
  14. package/dist/types/resources/index.d.ts +19 -8
  15. package/dist/types/resources/ipLookup.d.ts +20 -4
  16. package/dist/types/resources/jobs.d.ts +62 -21
  17. package/dist/types/resources/library.d.ts +1435 -0
  18. package/dist/types/resources/linkTrees.d.ts +142 -30
  19. package/dist/types/resources/media.d.ts +351 -0
  20. package/dist/types/resources/movies.d.ts +1186 -0
  21. package/dist/types/resources/music/artists.d.ts +1066 -0
  22. package/dist/types/resources/music/imports.d.ts +940 -0
  23. package/dist/types/resources/music/index.d.ts +61 -0
  24. package/dist/types/resources/music/playlists.d.ts +1026 -0
  25. package/dist/types/resources/music/social.d.ts +1132 -0
  26. package/dist/types/resources/music/songs.d.ts +1183 -0
  27. package/dist/types/resources/notepads.d.ts +4 -1
  28. package/dist/types/resources/quotas.d.ts +7 -1
  29. package/dist/types/resources/realtime.d.ts +855 -0
  30. package/dist/types/resources/shortLinks.d.ts +45 -4
  31. package/dist/types/resources/social.d.ts +1330 -0
  32. package/dist/types/resources/storage/upload.d.ts +158 -11
  33. package/dist/types/resources/storage.d.ts +88 -22
  34. package/dist/types/resources/tickets.d.ts +82 -3
  35. package/dist/types/resources/tools/backgroundRemoval.d.ts +18 -3
  36. package/dist/types/resources/tools/captions.d.ts +448 -21
  37. package/dist/types/resources/tools/downloader.d.ts +21 -0
  38. package/dist/types/resources/tools/index.d.ts +57 -15
  39. package/dist/types/resources/tools/jumpstyle.d.ts +50 -17
  40. package/dist/types/resources/tools/transcription.d.ts +35 -13
  41. package/dist/types/resources/tools/upscale.d.ts +23 -3
  42. package/dist/types/resources/tools/vocalSeparation.d.ts +30 -13
  43. package/dist/types/types.d.ts +249 -17
  44. package/package.json +2 -1
@@ -46,24 +46,57 @@ import { type EditsQuota, type ToolCaptcha, type ToolRecord, type ToolRunOptions
46
46
  export type JumpstyleDensity = "chill" | "normal" | "hyper";
47
47
  /** The three the backend accepts. Anything else is silently read as `"normal"`. */
48
48
  export declare const JUMPSTYLE_DENSITIES: readonly JumpstyleDensity[];
49
- /** A jumpstyle edit. */
49
+ /**
50
+ * Lifecycle of a jumpstyle edit.
51
+ *
52
+ * Narrower than {@link ToolStatus}: `JumpstyleJob::STATUSES` has only three
53
+ * entries and `"pending"` is not one of them. The controller forwards the files
54
+ * to the sidecar inside the create request and saves the row already
55
+ * `"processing"`, so there is no state in which an edit exists but has not
56
+ * started.
57
+ */
58
+ export type JumpstyleStatus = "processing" | "complete" | "failed";
59
+ /**
60
+ * A jumpstyle edit.
61
+ *
62
+ * Both routes that answer with one - `POST /jumpstyle_jobs` and
63
+ * `GET /jumpstyle_jobs/:id` - render the `:extended` view, so every key below
64
+ * is present on every response.
65
+ *
66
+ * The first four columns are nullable in the schema but written by the
67
+ * controller on every create, so a row the API can produce always carries
68
+ * them. `density` and `rapid_fire` are `NOT NULL` with defaults on top of that.
69
+ */
50
70
  export interface JumpstyleJob extends ToolRecord {
51
- readonly track_filename?: string | null;
52
- readonly n_clips?: number | null;
53
- /** Seconds of output. */
54
- readonly duration?: number | null;
55
- /** The seed that produced this cut. Reuse it to reproduce the edit. */
56
- readonly seed?: number | null;
57
- readonly density?: string | null;
58
- readonly rapid_fire?: boolean | null;
59
- /** BPM that was forced, when one was. */
60
- readonly bpm?: number | null;
61
- /** BPM the sidecar detected, or the forced one once the run settles. */
62
- readonly detected_bpm?: number | null;
63
- /** What the sidecar is doing right now. `null` unless processing. */
64
- readonly stage?: string | null;
65
- /** The finished video, once the status is `"complete"`. */
66
- readonly output_url?: string | null;
71
+ readonly status: JumpstyleStatus;
72
+ /** Uploaded track's name; `"track"` when the upload carried none. */
73
+ readonly track_filename: string;
74
+ /** How many clips were sent. At most 20. */
75
+ readonly n_clips: number;
76
+ /** Seconds of output, clamped by the server to `[10, 60]`. */
77
+ readonly duration: number;
78
+ /**
79
+ * The seed that produced this cut. Reuse it to reproduce the edit - it is
80
+ * the ONLY way to, and the server rolls a fresh one whenever it is not
81
+ * given one.
82
+ */
83
+ readonly seed: number;
84
+ /** `NOT NULL DEFAULT 'normal'`. An unrecognised request value lands here as `"normal"`. */
85
+ readonly density: JumpstyleDensity | string;
86
+ /** `NOT NULL DEFAULT false`. */
87
+ readonly rapid_fire: boolean;
88
+ /** BPM that was forced at create time, or `null` when detection was left to run. */
89
+ readonly bpm: number | null;
90
+ /**
91
+ * While processing: the BPM the sidecar has detected, or `null` if it has not
92
+ * yet. Once the row settles: whatever `bpm` holds, forced or not. So this
93
+ * changes meaning at the moment the run ends.
94
+ */
95
+ readonly detected_bpm: number | null;
96
+ /** What the sidecar is doing right now, read live. `null` unless processing. */
97
+ readonly stage: string | null;
98
+ /** Signed URL of the finished video once complete and attached, `null` otherwise. */
99
+ readonly output_url: string | null;
67
100
  }
68
101
  /** Arguments for starting an edit. */
69
102
  export interface CreateJumpstyleInput extends ToolCaptcha {
@@ -36,22 +36,44 @@
36
36
  import { Resource } from "../../http";
37
37
  import type { FileInput, Id, RequestOptions } from "../../types";
38
38
  import { type SecondsQuota, type ToolCaptcha, type ToolModel, type ToolRecord, type ToolRunOptions } from "./index";
39
- /** A transcription run. */
39
+ /**
40
+ * A transcription run.
41
+ *
42
+ * Both routes that answer with one - `POST /transcriptions` and
43
+ * `GET /transcriptions/:id` - render the `:extended` view, so every key below
44
+ * is present on every response, the five `:extended` ones included. What
45
+ * changes as the run progresses is the VALUE, not the key set: polling a
46
+ * pending run and polling a finished one return the same shape.
47
+ */
40
48
  export interface Transcription extends ToolRecord {
49
+ /** Never `null`: the column is `NOT NULL` and the controller rejects an
50
+ * unknown model with a 400 before the row is built. */
41
51
  readonly model_id: string;
42
- /** Audio duration charged against the quota. */
52
+ /**
53
+ * Audio duration charged against the quota, rounded UP at create time. Never
54
+ * `null` (`NOT NULL DEFAULT 0`), and it is what was spent even if the run
55
+ * later fails.
56
+ */
43
57
  readonly duration_seconds: number;
44
- /** Language that was requested, or `null` when it was auto-detected. */
45
- readonly language?: string | null;
46
- /** Language the model actually detected. */
47
- readonly detected_language?: string | null;
48
- readonly has_original?: boolean;
49
- /** The transcript. Only shipped once the status is `"complete"`. */
50
- readonly text?: string | null;
51
- /** SubRip subtitles, once complete. */
52
- readonly srt_url?: string | null;
53
- /** WebVTT subtitles, once complete. */
54
- readonly vtt_url?: string | null;
58
+ /** Language that was requested, or `null` when it was left to detection. */
59
+ readonly language: string | null;
60
+ /** Language the model actually detected, or `null` before it has. */
61
+ readonly detected_language: string | null;
62
+ /**
63
+ * Whether the uploaded audio is still attached. A real boolean, never
64
+ * `null`: the field is computed as `original_audio.attached?`.
65
+ */
66
+ readonly has_original: boolean;
67
+ /**
68
+ * The transcript, or `null`. Shipped ONLY once the status is `"complete"` -
69
+ * deliberately, so that polling a long run does not drag the whole text
70
+ * across the wire every few seconds.
71
+ */
72
+ readonly text: string | null;
73
+ /** Signed SubRip URL once complete and attached, `null` otherwise. */
74
+ readonly srt_url: string | null;
75
+ /** Signed WebVTT URL once complete and attached, `null` otherwise. */
76
+ readonly vtt_url: string | null;
55
77
  }
56
78
  /** Arguments for starting a transcription. */
57
79
  export interface CreateTranscriptionInput extends ToolCaptcha {
@@ -19,11 +19,31 @@ import { type ToolCaptcha, type ToolJobHandle, type ToolRecord, type ToolRunOpti
19
19
  * allow-list of strings; `4` and `"4"` are not the same request.
20
20
  */
21
21
  export type UpscaleScale = "2" | "3" | "4";
22
- /** An upscale run. */
22
+ /**
23
+ * An upscale run.
24
+ *
25
+ * Both routes that answer with one - `POST /upscales` and `GET /upscales/:id` -
26
+ * render the `:extended` view, so `result_url` is always PRESENT and simply
27
+ * `null` until the run completes. There is no default-view variant of this
28
+ * record reachable through the API.
29
+ *
30
+ * `progress_percent`, inherited from {@link ToolRecord}, is never sent for this
31
+ * tool: `UpscaleBlueprint` has no such field. Progress for an upscale lives on
32
+ * the {@link Job} row that {@link UpscaleCreated.job_id} names.
33
+ */
23
34
  export interface Upscale extends ToolRecord {
35
+ /**
36
+ * One of `"2"`, `"3"`, `"4"` - a string, because the column is a string and
37
+ * the allow-list is `%w[2 3 4]`. Never `null`: `NOT NULL DEFAULT '4'`.
38
+ */
24
39
  readonly scale: string;
25
- /** URL of the enlarged image, once the status is `"complete"`. */
26
- readonly result_url?: string | null;
40
+ /**
41
+ * Signed URL of the enlarged PNG, or `null`. `null` covers three different
42
+ * situations - the run has not finished, it failed, or the 24-hour sweep
43
+ * took the attachment - which is why {@link UpscaleNamespace.resultUrl}
44
+ * exists rather than a bare read of this field.
45
+ */
46
+ readonly result_url: string | null;
27
47
  }
28
48
  /** What `POST /upscales` answers with. */
29
49
  export type UpscaleCreated = Upscale & ToolJobHandle;
@@ -35,28 +35,45 @@ import type { FileInput, Id, Progress, RequestOptions } from "../../types";
35
35
  import { type SecondsQuota, type ToolCaptcha, type ToolModel, type ToolRecord, type ToolRunOptions } from "./index";
36
36
  /** Which stem to fetch. */
37
37
  export type VocalStem = "vocals" | "instrumental";
38
- /** A vocal separation run. */
38
+ /**
39
+ * A vocal separation run.
40
+ *
41
+ * Both routes that answer with one - `POST /vocal_separations` and
42
+ * `GET /vocal_separations/:id` - render the `:extended` view, so every key
43
+ * below is present on every response. Only the values move.
44
+ */
39
45
  export interface VocalSeparation extends ToolRecord {
46
+ /** Never `null`: `NOT NULL`, and an unknown model is a 400 before the row exists. */
40
47
  readonly model_id: string;
41
- /** Audio duration charged against the quota. */
48
+ /** Audio duration charged against the quota, rounded up at create time. */
42
49
  readonly duration_seconds: number;
43
- /** Set when the run came from the music library rather than an upload. */
44
- readonly song_id?: Id | null;
45
- readonly song_title?: string | null;
46
- readonly has_original?: boolean;
47
- readonly has_vocals?: boolean;
48
- readonly has_instrumental?: boolean;
50
+ /**
51
+ * Set when the run came from the music library rather than an upload.
52
+ *
53
+ * A **number**, not a string. `vocal_separations.song_id` is a `bigint`
54
+ * pointing at `songs`, which is one of the few tables in this API that kept
55
+ * an integer primary key - so this is the one id in the tools family that is
56
+ * not an {@link Id}. Comparing it against a string never matches.
57
+ */
58
+ readonly song_id: number | null;
59
+ /** Title of that song, or `null` for an uploaded run. */
60
+ readonly song_title: string | null;
61
+ /** Real booleans, never `null`: each is an `attached?` call on the record. */
62
+ readonly has_original: boolean;
63
+ readonly has_vocals: boolean;
64
+ readonly has_instrumental: boolean;
49
65
  /**
50
66
  * Live runs queued ahead of this one; `0` means next up. `null` once the run
51
67
  * is processing or terminal.
52
68
  */
53
- readonly queue_position?: number | null;
69
+ readonly queue_position: number | null;
54
70
  /**
55
- * Stem URLs, once complete. Both stay `null` for a song-owned separation,
56
- * whose stems live on the song as filesystem nodes instead.
71
+ * Signed stem URLs once complete, `null` otherwise - and permanently `null`
72
+ * for a song-owned separation, whose stems are written onto the song as
73
+ * filesystem nodes and never attached to this row.
57
74
  */
58
- readonly vocals_url?: string | null;
59
- readonly instrumental_url?: string | null;
75
+ readonly vocals_url: string | null;
76
+ readonly instrumental_url: string | null;
60
77
  }
61
78
  /** Arguments for starting a separation. */
62
79
  export interface CreateVocalSeparationInput extends ToolCaptcha {
@@ -2,8 +2,17 @@
2
2
  * Primitives shared by every namespace of the SDK.
3
3
  *
4
4
  * Nothing here touches the platform: no `node:*`, no `process`, no `console`.
5
- * Files are values (Blob / Uint8Array / ReadableStream), never paths - the core
6
- * has no filesystem. Turning a path into a {@link FileInput} is the CLI's job.
5
+ * Files are normally values (Blob / Uint8Array / ReadableStream), never paths -
6
+ * the core has no filesystem, and turning a path into a {@link FileInput} is the
7
+ * CLI's job.
8
+ *
9
+ * React Native is the one exception, and it is the platform's exception rather
10
+ * than a relaxation of ours: a file the user picked there is a
11
+ * {@link NativeFile} descriptor `{ uri, name, type }` whose bytes live behind a
12
+ * `file://` / `content://` / `ph://` URI that only the RN runtime can resolve.
13
+ * It is still not a path the SDK reads - the SDK never reads it. It is handed
14
+ * back to RN's own `FormData`, which resolves it natively while building the
15
+ * multipart body. See {@link FileInput} and {@link NativeFile}.
7
16
  */
8
17
  /** Any JSON value the API can send or receive. */
9
18
  export type Json = string | number | boolean | null | Json[] | {
@@ -19,21 +28,86 @@ export type JsonObject = {
19
28
  * behind an authenticating proxy.
20
29
  */
21
30
  export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
31
+ /**
32
+ * A file picked on React Native, exactly as the platform hands it over.
33
+ *
34
+ * This is the shape `expo-file-system`'s picker returns and the shape the app
35
+ * already builds by hand (`oms-music/src/features/settings/pickers.ts`,
36
+ * `features/playlist/artworkPicker.ts`), and it is a DESCRIPTOR, not bytes: the
37
+ * URI is a `file://`, `content://` or `ph://` handle into the device, and
38
+ * nothing in JavaScript can turn it into a `Blob` without a native module.
39
+ *
40
+ * The SDK therefore never reads it. It appends the object VERBATIM to a
41
+ * `FormData`, because RN's `FormData` is not the web one: `getParts()` sees an
42
+ * entry whose value is an object with a `uri` and emits a file part from it,
43
+ * and the native networking layer streams the file off disk while building the
44
+ * multipart body. Handing RN a `Blob` instead is the thing that does not work -
45
+ * on Android it silently uploads an empty or truncated part.
46
+ *
47
+ * The three fields are exactly the three RN reads. `size` is carried because
48
+ * pickers report it and the SDK's own ceilings want it, and it is harmless:
49
+ * RN spreads the object into the part and ignores what it does not know.
50
+ *
51
+ * ## Where it works, and where it does not
52
+ *
53
+ * - `postForm` / any multipart endpoint: yes, on RN. On a web or Bun runtime
54
+ * the same object is REJECTED with a `TypeError` rather than stringified into
55
+ * `"[object Object]"`, which is what a plain `FormData.append` would do to it.
56
+ * See `supportsNativeFormDataFiles` in `http.ts`.
57
+ * - Storage direct upload (`resources/storage/upload.ts`): NO. That path
58
+ * `PUT`s the bytes to a presigned URL and MD5s them first, and neither is
59
+ * possible without reading the file. On RN, read it into bytes first (Expo's
60
+ * `File#bytes()`), or use a native uploader. {@link readFileInput} throws a
61
+ * message saying so rather than failing at the object store.
62
+ */
63
+ export interface NativeFile {
64
+ /** Platform handle to the bytes: `file://`, `content://`, `ph://`. */
65
+ readonly uri: string;
66
+ /** Filename the server should store. RN sends it as the part's `filename`. */
67
+ readonly name: string;
68
+ /**
69
+ * MIME type. Pickers report `""` for some `content://` URIs, which is why the
70
+ * app falls back to a per-kind constant before it gets here; do the same, as
71
+ * Rails infers the container format from the part's content type for several
72
+ * of the tools.
73
+ */
74
+ readonly type?: string;
75
+ /** Byte length when the picker reported one. Ignored by RN, used by the SDK. */
76
+ readonly size?: number;
77
+ }
78
+ /**
79
+ * True when `value` is a {@link NativeFile} descriptor.
80
+ *
81
+ * The test is `uri` and `name` both being strings, which no other form value
82
+ * satisfies: a {@link FileInput} carries `data` + `filename`, and everything
83
+ * else in a form bag is a primitive. Deliberately tolerant of a missing `type`,
84
+ * because a `content://` pick on Android genuinely arrives without one and
85
+ * refusing it would lose the upload over a field RN treats as optional.
86
+ */
87
+ export declare function isNativeFile(value: unknown): value is NativeFile;
22
88
  /**
23
89
  * Bytes handed to the SDK for upload.
24
90
  *
25
- * `data` is a value, never a path. `filename` is required because the API
26
- * derives the stored name and, for some tools, the container format from it.
91
+ * `data` is a value, never a path - with the one platform exception,
92
+ * {@link NativeFile}, which is a descriptor RN resolves for us and which only
93
+ * works on a multipart endpoint. `filename` is required because the API derives
94
+ * the stored name and, for some tools, the container format from it.
27
95
  *
28
96
  * `ReadableStream` is accepted for symmetry with the platform, but note that
29
97
  * multipart form bodies have to be materialised: {@link readFileInput} buffers
30
98
  * a stream into a Blob before it can be appended to a `FormData`. For anything
31
99
  * large, prefer the storage direct-upload path, which streams straight to the
32
100
  * object store and never passes through Rails.
101
+ *
102
+ * On React Native there is no need to wrap a picked file in one of these at
103
+ * all: pass the picked `{ uri, name, type }` object straight into the form bag
104
+ * and the transport appends it verbatim. Wrapping it is for the case where the
105
+ * name the server should store differs from the name on the device, in which
106
+ * case `filename` wins and is what RN sends as the part's filename.
33
107
  */
34
108
  export interface FileInput {
35
- /** The bytes. */
36
- readonly data: Blob | Uint8Array | ReadableStream<Uint8Array>;
109
+ /** The bytes, or a {@link NativeFile} descriptor on React Native. */
110
+ readonly data: Blob | Uint8Array | ReadableStream<Uint8Array> | NativeFile;
37
111
  /** Name the server should store, e.g. `"take-3.wav"`. Required. */
38
112
  readonly filename: string;
39
113
  /** MIME type. Defaults to the Blob's own type, then `application/octet-stream`. */
@@ -60,6 +134,14 @@ export interface FileOutput {
60
134
  *
61
135
  * Buffers a `ReadableStream` fully - see the note on {@link FileInput}. Uses
62
136
  * only platform APIs, so it runs in an isolate.
137
+ *
138
+ * @throws {TypeError} for a {@link NativeFile}. There is no honest Blob to
139
+ * return: the bytes are behind a device URI that only the RN runtime can
140
+ * open, and returning an empty Blob would upload an empty file with a 200 on
141
+ * it. Multipart endpoints never reach here - `buildFormData` appends a native
142
+ * descriptor verbatim instead - so the throw belongs to the byte-hungry
143
+ * callers (the storage direct-upload driver, which must also MD5 the body),
144
+ * and it names the way out rather than just refusing.
63
145
  */
64
146
  export declare function readFileInput(input: FileInput): Promise<{
65
147
  blob: Blob;
@@ -69,8 +151,13 @@ export declare function readFileInput(input: FileInput): Promise<{
69
151
  /**
70
152
  * Convenience constructor for a {@link FileInput}. Prefer it over an object
71
153
  * literal so the `filename`-is-required rule stays visible at the call site.
154
+ *
155
+ * A {@link NativeFile} is accepted and needs this only when the server should
156
+ * store a different name than the device used; otherwise pass the picked object
157
+ * straight into the form bag. When wrapped, `filename` and `contentType` are
158
+ * what go on the wire - the descriptor's own `name` and `type` are overridden.
72
159
  */
73
- export declare function file(data: Blob | Uint8Array | ReadableStream<Uint8Array>, filename: string, options?: {
160
+ export declare function file(data: Blob | Uint8Array | ReadableStream<Uint8Array> | NativeFile, filename: string, options?: {
74
161
  contentType?: string;
75
162
  size?: number;
76
163
  }): FileInput;
@@ -79,6 +166,37 @@ export declare function file(data: Blob | Uint8Array | ReadableStream<Uint8Array
79
166
  *
80
167
  * `total` is `undefined` whenever the size is genuinely unknown (a stream
81
168
  * upload, a server-side render with no ETA). Do not fake it with a guess.
169
+ *
170
+ * ## What `phase: "upload"` can and cannot promise
171
+ *
172
+ * It ticks once per COMPLETED transfer, never per byte, and that is a property
173
+ * of `fetch` rather than a decision this SDK is free to revisit. No `fetch` -
174
+ * browser, React Native or Worker - exposes request-body progress. The web
175
+ * frontend gets a real byte counter in its 36 `onUploadProgress` call sites
176
+ * because axios is XHR underneath, and XHR is the only API that has ever
177
+ * reported bytes as they leave.
178
+ *
179
+ * There is therefore ONE mechanism in this SDK, not two: `resources/storage/upload.ts`
180
+ * ticks per finished transfer (one per 32 MiB part on the multipart tier, one
181
+ * per file on the direct tier), and a host that wants byte-level granularity
182
+ * hands an XHR-backed {@link FetchLike} to `UploadManagerOptions.fetch` and
183
+ * keeps the rest of the driver. That escape hatch is documented there with a
184
+ * working recipe, and it is available in exactly the runtimes that have XHR:
185
+ *
186
+ * - browser: yes, `xhr.upload.onprogress`;
187
+ * - React Native: yes. RN's own `fetch` is a thin layer over its `XMLHttpRequest`,
188
+ * and `xhr.upload.onprogress` fires there for both `Blob` and
189
+ * {@link NativeFile} parts - it is the same event `expo-file-system`'s
190
+ * uploader surfaces;
191
+ * - Worker isolate: no. There is no XHR, so per-transfer ticks are the ceiling.
192
+ *
193
+ * `supportsUploadProgress()` in `http.ts` answers that question at runtime so a
194
+ * caller can hide a byte-accurate bar rather than let it sit at 0% and jump.
195
+ *
196
+ * The thing not to do is count bytes as they are handed to the runtime: a
197
+ * stream request body reports what was buffered, not what was acknowledged,
198
+ * which is the lie that parks a bar at 100% for a minute. It also breaks the
199
+ * presigned PUTs outright - see the module note in `storage/upload.ts`.
82
200
  */
83
201
  export interface Progress {
84
202
  /** What is happening right now. */
@@ -100,15 +218,39 @@ export interface RequestOptions {
100
218
  */
101
219
  readonly signal?: AbortSignal;
102
220
  /**
103
- * Deadline in milliseconds for the whole call, retries included. Overrides
104
- * the client default. `0` disables the deadline.
221
+ * Deadline in milliseconds for ONE ATTEMPT. Overrides the client default.
222
+ * `0` disables it.
223
+ *
224
+ * Not the deadline for the whole call: the transport starts a fresh one per
225
+ * attempt, so a call that retries can take up to `maxAttempts` times this
226
+ * plus the backoff between them (and a `429` waits out `Retry-After`, which
227
+ * this API sets from a one-minute window). To bound the wall clock, pass a
228
+ * {@link RequestOptions.signal} you abort yourself, or `retry: false`.
229
+ *
230
+ * The deadline covers getting a response, not draining it: it is disposed as
231
+ * soon as the headers arrive, which is what lets `raw()` and `streamText()`
232
+ * hold a stream open for longer than `timeoutMs`. A stream needs its own
233
+ * silence limit instead - `streamText` has one.
105
234
  */
106
235
  readonly timeoutMs?: number;
107
236
  /** Extra request headers. Merged over the client's, under `Authorization`. */
108
237
  readonly headers?: Record<string, string>;
109
238
  /**
110
- * Per-call retry override. `false` disables retries entirely - pass it for
111
- * any non-idempotent create you would rather see fail than duplicate.
239
+ * Per-call retry override, and the ONLY way to put a mutating request in
240
+ * scope for a retry.
241
+ *
242
+ * By default only safe methods are replayed after an ambiguous failure (a
243
+ * torn connection, a 5xx); a `POST` is not, because a replay after a lost
244
+ * answer is how one create becomes two records. Passing an object here -
245
+ * `{}` is enough, it inherits {@link DEFAULT_RETRY} - says you have looked at
246
+ * this specific endpoint and decided a duplicate is acceptable.
247
+ *
248
+ * `false` disables retrying completely, `429` included. Pass it for a call
249
+ * that mints something under a fresh random identifier per attempt (a short
250
+ * link, a notepad, a chest): a retry there does not duplicate the answer, it
251
+ * leaves an orphan behind and hands you the wrong one.
252
+ *
253
+ * See the retry policy documented on `ApiClient`.
112
254
  */
113
255
  readonly retry?: RetryOptions | false;
114
256
  }
@@ -138,15 +280,75 @@ export interface ResolvedRetry {
138
280
  /** Defaults applied when a caller says nothing about retrying. */
139
281
  export declare const DEFAULT_RETRY: ResolvedRetry;
140
282
  /**
141
- * Query parameters. Nested objects and arrays are encoded the way Rails reads
142
- * them (`search[status]=open`, `ids[]=1&ids[]=2`) - see `encodeQuery` in
143
- * `http.ts`. `undefined` and `null` values are dropped, not sent as empty.
283
+ * One value in a query string.
284
+ *
285
+ * Nested objects and arrays are encoded the way Rails reads them
286
+ * (`search[status]=open`, `ids[]=1&ids[]=2`). Three values do NOT encode
287
+ * literally, and `encodeQuery` in `http.ts` carries the full argument:
288
+ *
289
+ * - `undefined` is dropped. It means "I am not filtering on this column".
290
+ * - `null` is sent as the backend's `\b` null sentinel and comes back out of
291
+ * `CrudActions` as SQL `NULL`. It means "filter where this column IS NULL".
292
+ * The two are not interchangeable, and getting them the wrong way round is
293
+ * the difference between one folder and somebody's entire tree.
294
+ * - a `Date` is sent as its ISO-8601 string, which is the only shape the Rails
295
+ * date filters parse (`String#to_date_safe`).
144
296
  */
145
- export type QueryValue = string | number | boolean | null | undefined | QueryValue[] | {
297
+ export type QueryValue = string | number | boolean | Date | null | undefined | QueryValue[] | {
146
298
  [key: string]: QueryValue;
147
299
  };
148
300
  /** A bag of query parameters. */
149
301
  export type QueryParams = Record<string, QueryValue>;
302
+ /**
303
+ * Hard ceiling the backend applies to a page size, mirroring
304
+ * `QueryModifier::MAX_PAGE_SIZE`.
305
+ *
306
+ * The server clamps silently - `size = [size, MAX_PAGE_SIZE].min` - so asking
307
+ * for 1200 returns 500 rows and no indication that a ceiling was hit. The SDK
308
+ * therefore clamps to the same number BEFORE the request, so that the size it
309
+ * reports back in {@link Paginated.pageSize} is the size the rows were actually
310
+ * counted against. See {@link resolvePageSize}.
311
+ */
312
+ export declare const MAX_PAGE_SIZE = 500;
313
+ /**
314
+ * Page size the SDK asks for when the caller says nothing.
315
+ *
316
+ * Deliberately below {@link MAX_PAGE_SIZE}: a `list()` is usually the first
317
+ * screen of something, and 500 rows of expanded records is a slow first paint.
318
+ * Note this is NOT the server's own default - a request with no page modifier
319
+ * at all gets `QueryModifier::DEFAULT_PAGE_SIZE` (500) forced on it by
320
+ * `CrudActions#index_modifiers_params` - but the SDK always sends one.
321
+ */
322
+ export declare const DEFAULT_PAGE_SIZE = 100;
323
+ /**
324
+ * Normalises a requested page size into the one the server will actually use.
325
+ *
326
+ * Two different failures are handled differently on purpose:
327
+ *
328
+ * - **above the ceiling** is clamped, not rejected. The request still succeeds
329
+ * and paging still reaches every row, so failing it would break a working
330
+ * call for nothing. What must not happen is the caller being told it got the
331
+ * size it asked for: that is how `pageSize: 1200` yielded 500 items and
332
+ * `hasMore: false`, dropping 700 rows without a word.
333
+ * - **not a usable size at all** (`NaN`, `Infinity`, zero, negative) throws.
334
+ * There is nothing sensible to clamp such a value to, and it is not merely
335
+ * wrong on the client: `pageModifier` would put `"1:NaN"` on the wire,
336
+ * `QueryModifier#apply_pagination` reads that as size `0`, bails out before
337
+ * `limit`/`offset` are applied, and the endpoint answers with the WHOLE
338
+ * table. A typo would turn a listing into an unbounded scan holding a Puma
339
+ * thread and a DB connection.
340
+ *
341
+ * @throws {TypeError} when `pageSize` is not a finite number of at least 1.
342
+ */
343
+ export declare function resolvePageSize(pageSize?: number): number;
344
+ /**
345
+ * Normalises a requested page number. Pages are 1-based on the server
346
+ * (`offset = (number - 1) * size`), so page `0` is page 1 - and a
347
+ * {@link Paginated} that reported `page: 0` would fetch page 1 twice.
348
+ *
349
+ * @throws {TypeError} when `page` is not a finite number.
350
+ */
351
+ export declare function resolvePageNumber(page?: number): number;
150
352
  /**
151
353
  * Paging arguments accepted by every `list()` method.
152
354
  *
@@ -157,7 +359,26 @@ export type QueryParams = Record<string, QueryValue>;
157
359
  export interface PageParams {
158
360
  /** 1-based page number. Defaults to 1. */
159
361
  readonly page?: number;
160
- /** Items per page. Server maximum is 500. */
362
+ /**
363
+ * Items per page. Defaults to {@link DEFAULT_PAGE_SIZE}.
364
+ *
365
+ * {@link MAX_PAGE_SIZE} is the ceiling and it is enforced on both sides: ask
366
+ * for more and you get the ceiling, with {@link Paginated.pageSize} reporting
367
+ * the size you actually got rather than the one you asked for. To read more
368
+ * than 500 rows, page through them - `collect` and `pages` do it for you.
369
+ *
370
+ * A size the server could not parse is NOT clamped, it throws: `0`, a
371
+ * negative, `NaN` and `Infinity` all raise a `TypeError` before the request
372
+ * is built. This is deliberate and it is not defensive tidiness. `"1:NaN"`
373
+ * on the wire makes `QueryModifier#apply_pagination` read the size as zero
374
+ * and bail out before `limit`/`offset` are applied, so the endpoint answers
375
+ * with the WHOLE table: one typo turns a listing into an unbounded scan
376
+ * holding a Puma thread and a database connection. Failing at the call site
377
+ * is the only place that mistake is still cheap.
378
+ *
379
+ * Narrower than 0.2.0, which clamped `0` to 1 and let `NaN` through onto
380
+ * the wire. See {@link resolvePageSize}.
381
+ */
161
382
  readonly pageSize?: number;
162
383
  /** `"column:asc"` or `"column:desc"`, passed through as `modifiers[order]`. */
163
384
  readonly order?: string;
@@ -176,7 +397,12 @@ export interface Paginated<T> {
176
397
  readonly items: T[];
177
398
  /** 1-based number of this page. */
178
399
  readonly page: number;
179
- /** Page size that was requested. */
400
+ /**
401
+ * The page size the server actually applied, which is NOT necessarily the
402
+ * one that was requested: it is clamped to {@link MAX_PAGE_SIZE}. Read this
403
+ * rather than the number you passed in - they differ exactly when the
404
+ * difference matters.
405
+ */
180
406
  readonly pageSize: number;
181
407
  /** True when this page came back full, so another page may exist. */
182
408
  readonly hasMore: boolean;
@@ -189,6 +415,12 @@ export type PageLoader<T> = (params: Required<Pick<PageParams, "page" | "pageSiz
189
415
  * Builds a {@link Paginated} from the raw array the API returned plus the
190
416
  * loader that can fetch the next page. Resource modules use this instead of
191
417
  * hand-rolling the shape.
418
+ *
419
+ * @throws {TypeError} when `pageSize` is not a finite number of at least 1, or
420
+ * `page` is not finite. Both go through {@link resolvePageSize} /
421
+ * {@link resolvePageNumber}, which reject rather than clamp - see
422
+ * {@link PageParams.pageSize} for why an unparseable size is dangerous
423
+ * enough to be worth a throw.
192
424
  */
193
425
  export declare function createPage<T>(items: T[], page: number, pageSize: number, load: PageLoader<T>): Paginated<T>;
194
426
  /** Walks every page of a listing, yielding one page at a time. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omelhorsite/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "TypeScript SDK for the omelhorsite API. Isolate-safe: no node builtins, no environment access, no stdout.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -25,6 +25,7 @@
25
25
  "typecheck:tests": "tsc --noEmit -p tsconfig.test.json"
26
26
  },
27
27
  "devDependencies": {
28
+ "@types/bun": "^1.4.0",
28
29
  "typescript": "^5.9.3"
29
30
  },
30
31
  "dependencies": {