@dloizides/content-upload 1.0.1 → 1.2.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/CHANGELOG.md CHANGED
@@ -1,13 +1,38 @@
1
- # Changelog
2
-
3
- ## 1.0.0
4
-
5
- Initial release (KEFI-PEOPLE-1 "Organizer people editor", task T3).
6
-
7
- - `uploadImage(file, options)`: multipart POST to a consumer-supplied endpoint; CSRF header,
8
- credentials, client-side validation (empty / `maxBytes` / `allowedTypes`), abort, progress over
9
- XMLHttpRequest, typed `UploadError` with `UploadErrorKind`.
10
- - `buildUploadRequest` for pinning the write shape (no Content-Type on FormData).
11
- - `blobFromUri(uri)`.
12
- - `useImageUpload(options)` hook: `UploadStatus` idle / uploading / error / done, progress,
13
- supersede-on-new-upload, abort on reset and unmount.
1
+ # Changelog
2
+
3
+ ## 1.2.0
4
+
5
+ RUPEP-5 "Upload the RuPEP file from the Jobs tab" -- resumable tus upload.
6
+
7
+ - `uploadResumable({ endpoint, file, metadata?, gzip?, chunkSize?, headers?, withCredentials?, onProgress?, signal?, uploadFactory? })`
8
+ resolves the tus upload id (last segment of the upload URL). Built on `tus-js-client` 4 (MIT).
9
+ - 16 MiB chunks by default; cookies sent (`withCredentials`) unless `withCredentials: false`; `headers` carry the CSRF header.
10
+ - `gzip: true` compresses with `CompressionStream` as a stream and uses tus deferred length (`encoding=gzip`).
11
+ Where `CompressionStream` is missing the file is uploaded as-is with `encoding=identity`.
12
+ - Resume: a fingerprint of endpoint + size + lastModified + metadata is stored; the newest stored upload is resumed.
13
+ A gzip resume re-compresses from the start and skips the bytes already on the server (same engine = same bytes).
14
+ - `signal` abort terminates the server upload (tus termination) and rejects with `UploadErrorKind.Aborted`.
15
+ - Progress for gzip uploads counts source bytes read and holds at 99 until the upload finishes.
16
+
17
+ ## 1.1.0
18
+
19
+ RUPEP-5 "Upload the RuPEP file from the Jobs tab".
20
+
21
+ - `uploadFormWithProgress({ url, parts, headers?, onProgress?, signal?, withCredentials?, fetchImpl?, xhrFactory? })`:
22
+ generic multipart POST with progress; resolves parsed JSON (`null` for an empty 2xx body).
23
+ Credentials default to `include`; any Content-Type header is dropped; CSRF goes in `headers`.
24
+ - `buildFormRequest(options)` to pin the write shape.
25
+ - `UploadError.body` carries the raw response body of an `Http` failure (both transports).
26
+ - `uploadImage` now shares the transport selection (`sendUploadRequest`); behaviour unchanged.
27
+
28
+ ## 1.0.0
29
+
30
+ Initial release (KEFI-PEOPLE-1 "Organizer people editor", task T3).
31
+
32
+ - `uploadImage(file, options)`: multipart POST to a consumer-supplied endpoint; CSRF header,
33
+ credentials, client-side validation (empty / `maxBytes` / `allowedTypes`), abort, progress over
34
+ XMLHttpRequest, typed `UploadError` with `UploadErrorKind`.
35
+ - `buildUploadRequest` for pinning the write shape (no Content-Type on FormData).
36
+ - `blobFromUri(uri)`.
37
+ - `useImageUpload(options)` hook: `UploadStatus` idle / uploading / error / done, progress,
38
+ supersede-on-new-upload, abort on reset and unmount.
package/README.md CHANGED
@@ -1,63 +1,101 @@
1
- # @dloizides/content-upload
2
-
3
- Image upload for the dloizides.com RN-web portals: one multipart POST to the ContentService
4
- through a same-origin BFF proxy, plus a React hook with `idle / uploading / error / done` states.
5
- Merged from kefi-web `uploadContent.ts` and katalogos-web `useUploadContent.ts`.
6
-
7
- ## Install
8
-
9
- ```bash
10
- npm install @dloizides/content-upload
11
- ```
12
-
13
- Peer dependency: `react >= 18` (only for the hook).
14
-
15
- ## Client
16
-
17
- ```ts
18
- import { uploadImage, UploadError, UploadErrorKind } from '@dloizides/content-upload';
19
-
20
- const content = await uploadImage(file, {
21
- endpoint: '/bff/api/content/api/v1/content/upload', // consumer-supplied
22
- maxBytes: 5 * 1024 * 1024,
23
- allowedTypes: ['image/png', 'image/jpeg', 'image/webp'],
24
- });
25
- // content = { contentId, status, url }
26
- ```
27
-
28
- The request is `multipart/form-data` with fields `File`, `Category` (default `Image`) and
29
- `IsPublic` (default `true`), `credentials: 'include'` and the `X-BFF-Csrf: 1` anti-forgery header
30
- (`csrf: null` disables it, `csrf: { name, value }` replaces it). **No Content-Type is ever set**:
31
- the runtime adds the multipart boundary, and a JSON content type on a FormData body is a 415 (the
32
- same rule `@dloizides/bff-web-client`'s `writeContentTypeGuard` enforces for axios).
33
-
34
- Every failure throws `UploadError` with a `kind`: `Validation`, `Network`, `Http` (`status` set),
35
- `Aborted`, `InvalidResponse`. Map `kind` to translated copy; `message` is developer-facing.
36
-
37
- Transport: with `onProgress` and no `fetchImpl`, the upload uses `XMLHttpRequest` (the only
38
- transport that reports upload progress, available in browsers and React Native). Otherwise it
39
- uses `fetch`. `signal` aborts either.
40
-
41
- `blobFromUri(uri)` reads a `blob:` / `data:` / RN picker `file:` URI into a `Blob`.
42
-
43
- ## Hook
44
-
45
- ```ts
46
- import { useImageUpload, UploadStatus } from '@dloizides/content-upload';
47
-
48
- const { status, progress, error, result, upload, reset } = useImageUpload({
49
- endpoint: '/bff/api/content/api/v1/content/upload',
50
- onSuccess: (content) => setAvatarUrl(content.url),
51
- });
52
-
53
- await upload(file); // resolves to the content, or null on error / abort
54
- ```
55
-
56
- - A new `upload()` or `reset()` aborts the in-flight request; its late result is dropped.
57
- - Unmount aborts the in-flight request.
58
- - Options are read at call time, so an inline options object is fine.
59
-
60
- ## Scripts
61
-
62
- `npm run lint` · `npm run typecheck` · `npm run test:coverage` (100% threshold) · `npm run build`.
63
- Publish only via `./publish.ps1 -Bump <patch|minor|major>`.
1
+ # @dloizides/content-upload
2
+
3
+ Image upload for the dloizides.com RN-web portals: one multipart POST to the ContentService
4
+ through a same-origin BFF proxy, plus a React hook with `idle / uploading / error / done` states.
5
+ Merged from kefi-web `uploadContent.ts` and katalogos-web `useUploadContent.ts`.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @dloizides/content-upload
11
+ ```
12
+
13
+ Peer dependency: `react >= 18` (only for the hook).
14
+
15
+ ## Client
16
+
17
+ ```ts
18
+ import { uploadImage, UploadError, UploadErrorKind } from '@dloizides/content-upload';
19
+
20
+ const content = await uploadImage(file, {
21
+ endpoint: '/bff/api/content/api/v1/content/upload', // consumer-supplied
22
+ maxBytes: 5 * 1024 * 1024,
23
+ allowedTypes: ['image/png', 'image/jpeg', 'image/webp'],
24
+ });
25
+ // content = { contentId, status, url }
26
+ ```
27
+
28
+ The request is `multipart/form-data` with fields `File`, `Category` (default `Image`) and
29
+ `IsPublic` (default `true`), `credentials: 'include'` and the `X-BFF-Csrf: 1` anti-forgery header
30
+ (`csrf: null` disables it, `csrf: { name, value }` replaces it). **No Content-Type is ever set**:
31
+ the runtime adds the multipart boundary, and a JSON content type on a FormData body is a 415 (the
32
+ same rule `@dloizides/bff-web-client`'s `writeContentTypeGuard` enforces for axios).
33
+
34
+ Every failure throws `UploadError` with a `kind`: `Validation`, `Network`, `Http` (`status` set),
35
+ `Aborted`, `InvalidResponse`. Map `kind` to translated copy; `message` is developer-facing.
36
+
37
+ Transport: with `onProgress` and no `fetchImpl`, the upload uses `XMLHttpRequest` (the only
38
+ transport that reports upload progress, available in browsers and React Native). Otherwise it
39
+ uses `fetch`. `signal` aborts either.
40
+
41
+ `blobFromUri(uri)` reads a `blob:` / `data:` / RN picker `file:` URI into a `Blob`.
42
+
43
+ ## Generic multipart upload with progress
44
+
45
+ ```ts
46
+ import { uploadFormWithProgress, UploadError, UploadErrorKind } from '@dloizides/content-upload';
47
+
48
+ const result = await uploadFormWithProgress<{ jobId: string }>({
49
+ url: '/bff/api/aml/admin/jobs/rupep/upload',
50
+ parts: { file },
51
+ headers: { 'X-BFF-Csrf': '1' },
52
+ onProgress: (pct) => setPct(pct),
53
+ signal: controller.signal,
54
+ });
55
+ ```
56
+
57
+ Cookies are sent by default (`withCredentials: false` to opt out). A Content-Type header is
58
+ dropped. An `Http` failure carries `error.status` and the raw `error.body`; an empty 2xx body
59
+ resolves `null`.
60
+
61
+ ## Hook
62
+
63
+ ```ts
64
+ import { useImageUpload, UploadStatus } from '@dloizides/content-upload';
65
+
66
+ const { status, progress, error, result, upload, reset } = useImageUpload({
67
+ endpoint: '/bff/api/content/api/v1/content/upload',
68
+ onSuccess: (content) => setAvatarUrl(content.url),
69
+ });
70
+
71
+ await upload(file); // resolves to the content, or null on error / abort
72
+ ```
73
+
74
+ - A new `upload()` or `reset()` aborts the in-flight request; its late result is dropped.
75
+ - Unmount aborts the in-flight request.
76
+ - Options are read at call time, so an inline options object is fine.
77
+
78
+ ## Scripts
79
+
80
+ `npm run lint` · `npm run typecheck` · `npm run test:coverage` (100% threshold) · `npm run build`.
81
+ Publish only via `./publish.ps1 -Bump <patch|minor|major>`.
82
+
83
+ ## Resumable upload (tus)
84
+
85
+ ```ts
86
+ const id = await uploadResumable({
87
+ endpoint: '/bff/api/aml/v1/ingest/uploads',
88
+ file,
89
+ metadata: { part: 'persons' },
90
+ gzip: true,
91
+ headers: { 'X-BFF-Csrf': '1' },
92
+ onProgress: setPercent,
93
+ signal: controller.signal,
94
+ });
95
+ ```
96
+
97
+ - tus 1.0 via `tus-js-client`; 16 MiB chunks; cookies sent by default.
98
+ - `gzip: true` streams through `CompressionStream` with deferred length (`encoding=gzip`); without
99
+ `CompressionStream` the file goes up uncompressed (`encoding=identity`). The server reads `encoding`.
100
+ - Interrupted uploads resume from a stored fingerprint on the next call with the same file and metadata.
101
+ - Aborting terminates the server-side upload.
package/dist/index.d.mts CHANGED
@@ -1,3 +1,5 @@
1
+ import { UploadOptions, PreviousUpload } from 'tus-js-client';
2
+
1
3
  /** A header the BFF anti-forgery middleware requires on mutating requests. */
2
4
  interface CsrfHeader {
3
5
  name: string;
@@ -15,7 +17,7 @@ interface UploadedContent {
15
17
  /** Receives whole-number upload progress, 0-100. */
16
18
  type ProgressListener = (percent: number) => void;
17
19
  /** Options for `uploadImage`. Only `endpoint` is required. */
18
- interface UploadImageOptions {
20
+ interface UploadImageOptions extends TransportOptions {
19
21
  /** Same-origin URL of the upload endpoint, e.g. `/bff/api/content/api/v1/content/upload`. */
20
22
  endpoint: string;
21
23
  /** Filename sent with the file part. Defaults to `File.name`, else `"upload"`. */
@@ -36,15 +38,19 @@ interface UploadImageOptions {
36
38
  allowedTypes?: readonly string[];
37
39
  /** Aborts the request; the promise rejects with `UploadErrorKind.Aborted`. */
38
40
  signal?: AbortSignal;
39
- /**
40
- * Progress listener. When set (and no `fetchImpl` is given) the upload goes over
41
- * XMLHttpRequest, the only browser/RN transport that reports upload progress.
42
- */
43
- onProgress?: ProgressListener;
44
- /** Custom fetch (tests, SSR). Forces the fetch transport. */
45
- fetchImpl?: typeof fetch;
46
- /** Custom XMLHttpRequest factory (tests). */
47
- xhrFactory?: () => XMLHttpRequest;
41
+ }
42
+ /** Options for `uploadFormWithProgress`. Only `url` and `parts` are required. */
43
+ interface UploadFormOptions extends TransportOptions {
44
+ /** Upload endpoint, e.g. a same-origin BFF route. */
45
+ url: string;
46
+ /** Multipart fields in insertion order; a Blob/File becomes a file part. */
47
+ parts: Record<string, Blob | string>;
48
+ /** Extra headers (e.g. a CSRF header). Any Content-Type is dropped: the runtime sets the boundary. */
49
+ headers?: Record<string, string>;
50
+ /** Send cookies (BFF session). Default `true`. */
51
+ withCredentials?: boolean;
52
+ /** Aborts the request; the promise rejects with `UploadErrorKind.Aborted`. */
53
+ signal?: AbortSignal;
48
54
  }
49
55
 
50
56
  /** A fully built multipart upload, independent of the transport that sends it. */
@@ -56,6 +62,15 @@ interface UploadRequest {
56
62
  credentials: RequestCredentials;
57
63
  signal: AbortSignal | undefined;
58
64
  }
65
+ /** Transport selection knobs shared by every upload entry point. */
66
+ interface TransportOptions {
67
+ /** When set (and no `fetchImpl`), the upload goes over XMLHttpRequest to report progress. */
68
+ onProgress?: ProgressListener;
69
+ /** Custom fetch (tests, SSR). Forces the fetch transport. */
70
+ fetchImpl?: typeof fetch;
71
+ /** Custom XMLHttpRequest factory (tests). */
72
+ xhrFactory?: () => XMLHttpRequest;
73
+ }
59
74
 
60
75
  /** The Bff.AspNetCore anti-forgery convention: header presence, any value. */
61
76
  declare const DEFAULT_CSRF_HEADER: CsrfHeader;
@@ -72,6 +87,16 @@ declare function buildUploadRequest(file: Blob, options: UploadImageOptions): Up
72
87
  */
73
88
  declare function uploadImage(file: Blob, options: UploadImageOptions): Promise<UploadedContent>;
74
89
 
90
+ /** Build the transport-independent request. Exported so consumers can pin the write shape. */
91
+ declare function buildFormRequest(options: UploadFormOptions): UploadRequest;
92
+ /**
93
+ * POST arbitrary multipart parts with optional upload progress; resolves the parsed JSON body
94
+ * (`null` for an empty 2xx body).
95
+ *
96
+ * @throws {UploadError} `Http` carries `status` and the raw `body`; switch on `error.kind`.
97
+ */
98
+ declare function uploadFormWithProgress<T = unknown>(options: UploadFormOptions): Promise<T | null>;
99
+
75
100
  /**
76
101
  * Read a local URI (`blob:`, `data:`, or an RN image-picker `file:` URI) into a Blob so it
77
102
  * can be passed to `uploadImage`.
@@ -107,7 +132,9 @@ declare class UploadError extends Error {
107
132
  readonly kind: UploadErrorKind;
108
133
  /** HTTP status for `UploadErrorKind.Http`; undefined otherwise. */
109
134
  readonly status: number | undefined;
110
- constructor(kind: UploadErrorKind, message: string, status?: number);
135
+ /** Raw response body for `UploadErrorKind.Http`; undefined otherwise. */
136
+ readonly body: string | undefined;
137
+ constructor(kind: UploadErrorKind, message: string, status?: number, body?: string);
111
138
  }
112
139
  /** Normalise anything thrown into an `UploadError` (unknown throws become `Network`). */
113
140
  declare function toUploadError(cause: unknown): UploadError;
@@ -138,4 +165,66 @@ interface UseImageUploadResult extends ImageUploadState {
138
165
  */
139
166
  declare function useImageUpload(options: UseImageUploadOptions): UseImageUploadResult;
140
167
 
141
- export { type CsrfHeader, DEFAULT_CSRF_HEADER, type ImageUploadState, type ProgressListener, UploadError, UploadErrorKind, type UploadImageOptions, type UploadRequest, UploadStatus, type UploadedContent, type UseImageUploadOptions, type UseImageUploadResult, blobFromUri, buildUploadRequest, toUploadError, uploadImage, useImageUpload };
168
+ /** Value of the tus `encoding` metadata entry: how the uploaded bytes are encoded. */
169
+ declare const enum UploadEncoding {
170
+ Gzip = "gzip",
171
+ Identity = "identity"
172
+ }
173
+
174
+ /** The read side of a byte stream, as tus-js-client accepts it. */
175
+ type ByteReader = Pick<ReadableStreamDefaultReader<Uint8Array>, 'read'>;
176
+ /** What tus-js-client uploads, and how its progress maps back to the source file. */
177
+ interface ResumableSource {
178
+ input: Blob | ByteReader;
179
+ encoding: UploadEncoding;
180
+ lengthDeferred: boolean;
181
+ sourceBytesRead: () => number;
182
+ }
183
+
184
+ /** The part of a tus-js-client `Upload` that `uploadResumable` drives. */
185
+ interface TusUpload {
186
+ url: string | null;
187
+ start(): void;
188
+ abort(shouldTerminate?: boolean): Promise<void>;
189
+ findPreviousUploads(): Promise<PreviousUpload[]>;
190
+ resumeFromPreviousUpload(previousUpload: PreviousUpload): void;
191
+ }
192
+ /** Builds the tus upload; injectable for tests. */
193
+ type TusUploadFactory = (input: Blob | ByteReader, options: UploadOptions) => TusUpload;
194
+ /** Options for `uploadResumable`. Only `endpoint` and `file` are required. */
195
+ interface ResumableUploadOptions {
196
+ /** tus creation endpoint, e.g. `/bff/api/aml/v1/ingest/uploads`. */
197
+ endpoint: string;
198
+ file: Blob;
199
+ /** tus Upload-Metadata; `filename` defaults to `File.name`; `encoding` is always set by the client. */
200
+ metadata?: Record<string, string>;
201
+ /** Gzip the file as a stream (deferred length); falls back to identity when `CompressionStream` is missing. */
202
+ gzip?: boolean;
203
+ /** PATCH body size in bytes. Default 16 MiB (under the BFF 30 MB body limit). */
204
+ chunkSize?: number;
205
+ /** Extra request headers, e.g. the BFF CSRF header. */
206
+ headers?: Record<string, string>;
207
+ /** Send cookies (BFF session). Default `true`. */
208
+ withCredentials?: boolean;
209
+ onProgress?: ProgressListener;
210
+ /** Aborts and terminates the upload; the promise rejects with `UploadErrorKind.Aborted`. */
211
+ signal?: AbortSignal;
212
+ uploadFactory?: TusUploadFactory;
213
+ }
214
+
215
+ declare const DEFAULT_RESUMABLE_CHUNK_SIZE: number;
216
+ interface UploadCallbacks {
217
+ onProgress: (bytesSent: number, bytesTotal: number) => void;
218
+ onSuccess: () => void;
219
+ onError: (error: Error) => void;
220
+ }
221
+ /** Upload-Metadata sent on creation: `filename` from the File unless given, `encoding` always from the source. */
222
+ declare function buildResumableMetadata(file: Blob, metadata: Record<string, string> | undefined, encoding: UploadEncoding): Record<string, string>;
223
+ /** Stable resume key: the same endpoint, file identity and metadata resume the same tus upload. */
224
+ declare function resumeFingerprint(endpoint: string, file: Blob, metadata: Record<string, string>): string;
225
+ /** The tus-js-client options for one upload. Exported so consumers can pin the request shape. */
226
+ declare function buildTusOptions(options: ResumableUploadOptions, source: ResumableSource, callbacks: UploadCallbacks): UploadOptions;
227
+ /** tus 1.0 resumable upload (16 MiB chunks, optional streamed gzip, fingerprint resume); resolves the upload id. */
228
+ declare function uploadResumable(options: ResumableUploadOptions): Promise<string>;
229
+
230
+ export { type ByteReader, type CsrfHeader, DEFAULT_CSRF_HEADER, DEFAULT_RESUMABLE_CHUNK_SIZE, type ImageUploadState, type ProgressListener, type ResumableUploadOptions, type TransportOptions, type TusUpload, type TusUploadFactory, UploadEncoding, UploadError, UploadErrorKind, type UploadFormOptions, type UploadImageOptions, type UploadRequest, UploadStatus, type UploadedContent, type UseImageUploadOptions, type UseImageUploadResult, blobFromUri, buildFormRequest, buildResumableMetadata, buildTusOptions, buildUploadRequest, resumeFingerprint, toUploadError, uploadFormWithProgress, uploadImage, uploadResumable, useImageUpload };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { UploadOptions, PreviousUpload } from 'tus-js-client';
2
+
1
3
  /** A header the BFF anti-forgery middleware requires on mutating requests. */
2
4
  interface CsrfHeader {
3
5
  name: string;
@@ -15,7 +17,7 @@ interface UploadedContent {
15
17
  /** Receives whole-number upload progress, 0-100. */
16
18
  type ProgressListener = (percent: number) => void;
17
19
  /** Options for `uploadImage`. Only `endpoint` is required. */
18
- interface UploadImageOptions {
20
+ interface UploadImageOptions extends TransportOptions {
19
21
  /** Same-origin URL of the upload endpoint, e.g. `/bff/api/content/api/v1/content/upload`. */
20
22
  endpoint: string;
21
23
  /** Filename sent with the file part. Defaults to `File.name`, else `"upload"`. */
@@ -36,15 +38,19 @@ interface UploadImageOptions {
36
38
  allowedTypes?: readonly string[];
37
39
  /** Aborts the request; the promise rejects with `UploadErrorKind.Aborted`. */
38
40
  signal?: AbortSignal;
39
- /**
40
- * Progress listener. When set (and no `fetchImpl` is given) the upload goes over
41
- * XMLHttpRequest, the only browser/RN transport that reports upload progress.
42
- */
43
- onProgress?: ProgressListener;
44
- /** Custom fetch (tests, SSR). Forces the fetch transport. */
45
- fetchImpl?: typeof fetch;
46
- /** Custom XMLHttpRequest factory (tests). */
47
- xhrFactory?: () => XMLHttpRequest;
41
+ }
42
+ /** Options for `uploadFormWithProgress`. Only `url` and `parts` are required. */
43
+ interface UploadFormOptions extends TransportOptions {
44
+ /** Upload endpoint, e.g. a same-origin BFF route. */
45
+ url: string;
46
+ /** Multipart fields in insertion order; a Blob/File becomes a file part. */
47
+ parts: Record<string, Blob | string>;
48
+ /** Extra headers (e.g. a CSRF header). Any Content-Type is dropped: the runtime sets the boundary. */
49
+ headers?: Record<string, string>;
50
+ /** Send cookies (BFF session). Default `true`. */
51
+ withCredentials?: boolean;
52
+ /** Aborts the request; the promise rejects with `UploadErrorKind.Aborted`. */
53
+ signal?: AbortSignal;
48
54
  }
49
55
 
50
56
  /** A fully built multipart upload, independent of the transport that sends it. */
@@ -56,6 +62,15 @@ interface UploadRequest {
56
62
  credentials: RequestCredentials;
57
63
  signal: AbortSignal | undefined;
58
64
  }
65
+ /** Transport selection knobs shared by every upload entry point. */
66
+ interface TransportOptions {
67
+ /** When set (and no `fetchImpl`), the upload goes over XMLHttpRequest to report progress. */
68
+ onProgress?: ProgressListener;
69
+ /** Custom fetch (tests, SSR). Forces the fetch transport. */
70
+ fetchImpl?: typeof fetch;
71
+ /** Custom XMLHttpRequest factory (tests). */
72
+ xhrFactory?: () => XMLHttpRequest;
73
+ }
59
74
 
60
75
  /** The Bff.AspNetCore anti-forgery convention: header presence, any value. */
61
76
  declare const DEFAULT_CSRF_HEADER: CsrfHeader;
@@ -72,6 +87,16 @@ declare function buildUploadRequest(file: Blob, options: UploadImageOptions): Up
72
87
  */
73
88
  declare function uploadImage(file: Blob, options: UploadImageOptions): Promise<UploadedContent>;
74
89
 
90
+ /** Build the transport-independent request. Exported so consumers can pin the write shape. */
91
+ declare function buildFormRequest(options: UploadFormOptions): UploadRequest;
92
+ /**
93
+ * POST arbitrary multipart parts with optional upload progress; resolves the parsed JSON body
94
+ * (`null` for an empty 2xx body).
95
+ *
96
+ * @throws {UploadError} `Http` carries `status` and the raw `body`; switch on `error.kind`.
97
+ */
98
+ declare function uploadFormWithProgress<T = unknown>(options: UploadFormOptions): Promise<T | null>;
99
+
75
100
  /**
76
101
  * Read a local URI (`blob:`, `data:`, or an RN image-picker `file:` URI) into a Blob so it
77
102
  * can be passed to `uploadImage`.
@@ -107,7 +132,9 @@ declare class UploadError extends Error {
107
132
  readonly kind: UploadErrorKind;
108
133
  /** HTTP status for `UploadErrorKind.Http`; undefined otherwise. */
109
134
  readonly status: number | undefined;
110
- constructor(kind: UploadErrorKind, message: string, status?: number);
135
+ /** Raw response body for `UploadErrorKind.Http`; undefined otherwise. */
136
+ readonly body: string | undefined;
137
+ constructor(kind: UploadErrorKind, message: string, status?: number, body?: string);
111
138
  }
112
139
  /** Normalise anything thrown into an `UploadError` (unknown throws become `Network`). */
113
140
  declare function toUploadError(cause: unknown): UploadError;
@@ -138,4 +165,66 @@ interface UseImageUploadResult extends ImageUploadState {
138
165
  */
139
166
  declare function useImageUpload(options: UseImageUploadOptions): UseImageUploadResult;
140
167
 
141
- export { type CsrfHeader, DEFAULT_CSRF_HEADER, type ImageUploadState, type ProgressListener, UploadError, UploadErrorKind, type UploadImageOptions, type UploadRequest, UploadStatus, type UploadedContent, type UseImageUploadOptions, type UseImageUploadResult, blobFromUri, buildUploadRequest, toUploadError, uploadImage, useImageUpload };
168
+ /** Value of the tus `encoding` metadata entry: how the uploaded bytes are encoded. */
169
+ declare const enum UploadEncoding {
170
+ Gzip = "gzip",
171
+ Identity = "identity"
172
+ }
173
+
174
+ /** The read side of a byte stream, as tus-js-client accepts it. */
175
+ type ByteReader = Pick<ReadableStreamDefaultReader<Uint8Array>, 'read'>;
176
+ /** What tus-js-client uploads, and how its progress maps back to the source file. */
177
+ interface ResumableSource {
178
+ input: Blob | ByteReader;
179
+ encoding: UploadEncoding;
180
+ lengthDeferred: boolean;
181
+ sourceBytesRead: () => number;
182
+ }
183
+
184
+ /** The part of a tus-js-client `Upload` that `uploadResumable` drives. */
185
+ interface TusUpload {
186
+ url: string | null;
187
+ start(): void;
188
+ abort(shouldTerminate?: boolean): Promise<void>;
189
+ findPreviousUploads(): Promise<PreviousUpload[]>;
190
+ resumeFromPreviousUpload(previousUpload: PreviousUpload): void;
191
+ }
192
+ /** Builds the tus upload; injectable for tests. */
193
+ type TusUploadFactory = (input: Blob | ByteReader, options: UploadOptions) => TusUpload;
194
+ /** Options for `uploadResumable`. Only `endpoint` and `file` are required. */
195
+ interface ResumableUploadOptions {
196
+ /** tus creation endpoint, e.g. `/bff/api/aml/v1/ingest/uploads`. */
197
+ endpoint: string;
198
+ file: Blob;
199
+ /** tus Upload-Metadata; `filename` defaults to `File.name`; `encoding` is always set by the client. */
200
+ metadata?: Record<string, string>;
201
+ /** Gzip the file as a stream (deferred length); falls back to identity when `CompressionStream` is missing. */
202
+ gzip?: boolean;
203
+ /** PATCH body size in bytes. Default 16 MiB (under the BFF 30 MB body limit). */
204
+ chunkSize?: number;
205
+ /** Extra request headers, e.g. the BFF CSRF header. */
206
+ headers?: Record<string, string>;
207
+ /** Send cookies (BFF session). Default `true`. */
208
+ withCredentials?: boolean;
209
+ onProgress?: ProgressListener;
210
+ /** Aborts and terminates the upload; the promise rejects with `UploadErrorKind.Aborted`. */
211
+ signal?: AbortSignal;
212
+ uploadFactory?: TusUploadFactory;
213
+ }
214
+
215
+ declare const DEFAULT_RESUMABLE_CHUNK_SIZE: number;
216
+ interface UploadCallbacks {
217
+ onProgress: (bytesSent: number, bytesTotal: number) => void;
218
+ onSuccess: () => void;
219
+ onError: (error: Error) => void;
220
+ }
221
+ /** Upload-Metadata sent on creation: `filename` from the File unless given, `encoding` always from the source. */
222
+ declare function buildResumableMetadata(file: Blob, metadata: Record<string, string> | undefined, encoding: UploadEncoding): Record<string, string>;
223
+ /** Stable resume key: the same endpoint, file identity and metadata resume the same tus upload. */
224
+ declare function resumeFingerprint(endpoint: string, file: Blob, metadata: Record<string, string>): string;
225
+ /** The tus-js-client options for one upload. Exported so consumers can pin the request shape. */
226
+ declare function buildTusOptions(options: ResumableUploadOptions, source: ResumableSource, callbacks: UploadCallbacks): UploadOptions;
227
+ /** tus 1.0 resumable upload (16 MiB chunks, optional streamed gzip, fingerprint resume); resolves the upload id. */
228
+ declare function uploadResumable(options: ResumableUploadOptions): Promise<string>;
229
+
230
+ export { type ByteReader, type CsrfHeader, DEFAULT_CSRF_HEADER, DEFAULT_RESUMABLE_CHUNK_SIZE, type ImageUploadState, type ProgressListener, type ResumableUploadOptions, type TransportOptions, type TusUpload, type TusUploadFactory, UploadEncoding, UploadError, UploadErrorKind, type UploadFormOptions, type UploadImageOptions, type UploadRequest, UploadStatus, type UploadedContent, type UseImageUploadOptions, type UseImageUploadResult, blobFromUri, buildFormRequest, buildResumableMetadata, buildTusOptions, buildUploadRequest, resumeFingerprint, toUploadError, uploadFormWithProgress, uploadImage, uploadResumable, useImageUpload };