@speechall/sdk 1.0.0 → 2.0.4

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 (151) hide show
  1. package/.beads/README.md +81 -0
  2. package/.beads/config.yaml +62 -0
  3. package/.beads/issues.jsonl +46 -0
  4. package/.beads/metadata.json +4 -0
  5. package/.env.example +5 -0
  6. package/.fernignore +45 -0
  7. package/.gitattributes +3 -0
  8. package/.github/copilot-instructions.md +78 -0
  9. package/.github/workflows/auto-release-simple.yml.deprecated +106 -0
  10. package/.github/workflows/auto-release.yml +67 -0
  11. package/.github/workflows/ci.yml +41 -0
  12. package/.github/workflows/release.yml +57 -0
  13. package/AGENTS.md +94 -0
  14. package/CHANGELOG.md +58 -0
  15. package/CLAUDE.md +75 -0
  16. package/README.md +294 -155
  17. package/examples/CLAUDE.md +136 -0
  18. package/examples/advanced-options.ts +213 -0
  19. package/examples/basic-transcription.ts +66 -0
  20. package/examples/error-handling.ts +251 -0
  21. package/examples/list-models.ts +112 -0
  22. package/examples/remote-transcription.ts +60 -0
  23. package/fern/fern.config.json +4 -0
  24. package/fern/generators.yml +43 -0
  25. package/jest.config.js +11 -0
  26. package/package.json +26 -46
  27. package/regenerate.sh +45 -0
  28. package/scripts/fix-generated-code.sh +25 -0
  29. package/src/BaseClient.ts +82 -0
  30. package/src/Client.ts +30 -0
  31. package/src/api/errors/BadRequestError.ts +22 -0
  32. package/src/api/errors/GatewayTimeoutError.ts +22 -0
  33. package/src/api/errors/InternalServerError.ts +22 -0
  34. package/src/api/errors/NotFoundError.ts +22 -0
  35. package/src/api/errors/PaymentRequiredError.ts +22 -0
  36. package/src/api/errors/ServiceUnavailableError.ts +22 -0
  37. package/src/api/errors/TooManyRequestsError.ts +22 -0
  38. package/src/api/errors/UnauthorizedError.ts +22 -0
  39. package/src/api/errors/index.ts +8 -0
  40. package/src/api/index.ts +3 -0
  41. package/src/api/resources/index.ts +5 -0
  42. package/src/api/resources/replacementRules/client/Client.ts +148 -0
  43. package/src/api/resources/replacementRules/client/index.ts +1 -0
  44. package/src/api/resources/replacementRules/client/requests/CreateReplacementRulesetRequest.ts +25 -0
  45. package/src/api/resources/replacementRules/client/requests/index.ts +1 -0
  46. package/src/api/resources/replacementRules/index.ts +2 -0
  47. package/src/api/resources/replacementRules/types/CreateReplacementRulesetResponse.ts +6 -0
  48. package/src/api/resources/replacementRules/types/index.ts +1 -0
  49. package/src/api/resources/speechToText/client/Client.ts +275 -0
  50. package/src/api/resources/speechToText/client/index.ts +1 -0
  51. package/src/api/resources/speechToText/client/requests/RemoteTranscriptionConfiguration.ts +20 -0
  52. package/src/api/resources/speechToText/client/requests/TranscribeRequest.ts +26 -0
  53. package/src/api/resources/speechToText/client/requests/index.ts +2 -0
  54. package/src/api/resources/speechToText/index.ts +1 -0
  55. package/src/api/types/BaseTranscriptionConfiguration.ts +29 -0
  56. package/src/api/types/ErrorResponse.ts +11 -0
  57. package/src/api/types/ExactRule.ts +13 -0
  58. package/src/api/types/RegexGroupRule.ts +28 -0
  59. package/src/api/types/RegexRule.ts +28 -0
  60. package/src/api/types/ReplacementRule.ts +25 -0
  61. package/src/api/types/SpeechToTextModel.ts +90 -0
  62. package/src/api/types/TranscriptLanguageCode.ts +114 -0
  63. package/src/api/types/TranscriptOutputFormat.ts +18 -0
  64. package/src/api/types/TranscriptionDetailed.ts +19 -0
  65. package/src/api/types/TranscriptionModelIdentifier.ts +80 -0
  66. package/src/api/types/TranscriptionOnlyText.ts +11 -0
  67. package/src/api/types/TranscriptionProvider.ts +23 -0
  68. package/src/api/types/TranscriptionResponse.ts +8 -0
  69. package/src/api/types/TranscriptionSegment.ts +17 -0
  70. package/src/api/types/TranscriptionWord.ts +17 -0
  71. package/src/api/types/index.ts +16 -0
  72. package/src/auth/BearerAuthProvider.ts +37 -0
  73. package/src/auth/index.ts +1 -0
  74. package/src/core/auth/AuthProvider.ts +6 -0
  75. package/src/core/auth/AuthRequest.ts +9 -0
  76. package/src/core/auth/BasicAuth.ts +32 -0
  77. package/src/core/auth/BearerToken.ts +20 -0
  78. package/src/core/auth/NoOpAuthProvider.ts +8 -0
  79. package/src/core/auth/index.ts +5 -0
  80. package/src/core/base64.ts +27 -0
  81. package/src/core/exports.ts +2 -0
  82. package/src/core/fetcher/APIResponse.ts +23 -0
  83. package/src/core/fetcher/BinaryResponse.ts +34 -0
  84. package/src/core/fetcher/EndpointMetadata.ts +13 -0
  85. package/src/core/fetcher/EndpointSupplier.ts +14 -0
  86. package/src/core/fetcher/Fetcher.ts +391 -0
  87. package/src/core/fetcher/Headers.ts +93 -0
  88. package/src/core/fetcher/HttpResponsePromise.ts +116 -0
  89. package/src/core/fetcher/RawResponse.ts +61 -0
  90. package/src/core/fetcher/Supplier.ts +11 -0
  91. package/src/core/fetcher/createRequestUrl.ts +6 -0
  92. package/src/core/fetcher/getErrorResponseBody.ts +33 -0
  93. package/src/core/fetcher/getFetchFn.ts +3 -0
  94. package/src/core/fetcher/getHeader.ts +8 -0
  95. package/src/core/fetcher/getRequestBody.ts +20 -0
  96. package/src/core/fetcher/getResponseBody.ts +58 -0
  97. package/src/core/fetcher/index.ts +11 -0
  98. package/src/core/fetcher/makeRequest.ts +42 -0
  99. package/src/core/fetcher/requestWithRetries.ts +64 -0
  100. package/src/core/fetcher/signals.ts +26 -0
  101. package/src/core/file/exports.ts +1 -0
  102. package/src/core/file/file.ts +217 -0
  103. package/src/core/file/index.ts +2 -0
  104. package/src/core/file/types.ts +81 -0
  105. package/src/core/headers.ts +35 -0
  106. package/src/core/index.ts +7 -0
  107. package/src/core/json.ts +27 -0
  108. package/src/core/logging/exports.ts +19 -0
  109. package/src/core/logging/index.ts +1 -0
  110. package/src/core/logging/logger.ts +203 -0
  111. package/src/core/runtime/index.ts +1 -0
  112. package/src/core/runtime/runtime.ts +134 -0
  113. package/src/core/url/encodePathParam.ts +18 -0
  114. package/src/core/url/index.ts +3 -0
  115. package/src/core/url/join.ts +79 -0
  116. package/src/core/url/qs.ts +74 -0
  117. package/src/environments.ts +7 -0
  118. package/src/errors/SpeechallError.ts +58 -0
  119. package/src/errors/SpeechallTimeoutError.ts +13 -0
  120. package/src/errors/handleNonStatusCodeError.ts +37 -0
  121. package/src/errors/index.ts +2 -0
  122. package/src/exports.ts +1 -0
  123. package/src/index.ts +6 -0
  124. package/test-import.ts +17 -0
  125. package/tests/integration/api.test.ts +93 -0
  126. package/tests/unit/client.test.ts +91 -0
  127. package/tsconfig.json +20 -0
  128. package/dist/api.d.ts +0 -501
  129. package/dist/api.d.ts.map +0 -1
  130. package/dist/api.js +0 -610
  131. package/dist/base.d.ts +0 -32
  132. package/dist/base.d.ts.map +0 -1
  133. package/dist/base.js +0 -35
  134. package/dist/common.d.ts +0 -14
  135. package/dist/common.d.ts.map +0 -1
  136. package/dist/common.js +0 -91
  137. package/dist/configuration.d.ts +0 -23
  138. package/dist/configuration.d.ts.map +0 -1
  139. package/dist/configuration.js +0 -25
  140. package/dist/esm/api.js +0 -592
  141. package/dist/esm/base.js +0 -27
  142. package/dist/esm/common.js +0 -79
  143. package/dist/esm/configuration.js +0 -21
  144. package/dist/esm/example.js +0 -131
  145. package/dist/esm/index.js +0 -2
  146. package/dist/example.d.ts +0 -3
  147. package/dist/example.d.ts.map +0 -1
  148. package/dist/example.js +0 -133
  149. package/dist/index.d.ts +0 -3
  150. package/dist/index.d.ts.map +0 -1
  151. package/dist/index.js +0 -18
@@ -0,0 +1,61 @@
1
+ import { Headers } from "./Headers.js";
2
+
3
+ /**
4
+ * The raw response from the fetch call excluding the body.
5
+ */
6
+ export type RawResponse = Omit<
7
+ {
8
+ [K in keyof Response as Response[K] extends Function ? never : K]: Response[K]; // strips out functions
9
+ },
10
+ "ok" | "body" | "bodyUsed"
11
+ >; // strips out body and bodyUsed
12
+
13
+ /**
14
+ * A raw response indicating that the request was aborted.
15
+ */
16
+ export const abortRawResponse: RawResponse = {
17
+ headers: new Headers(),
18
+ redirected: false,
19
+ status: 499,
20
+ statusText: "Client Closed Request",
21
+ type: "error",
22
+ url: "",
23
+ } as const;
24
+
25
+ /**
26
+ * A raw response indicating an unknown error.
27
+ */
28
+ export const unknownRawResponse: RawResponse = {
29
+ headers: new Headers(),
30
+ redirected: false,
31
+ status: 0,
32
+ statusText: "Unknown Error",
33
+ type: "error",
34
+ url: "",
35
+ } as const;
36
+
37
+ /**
38
+ * Converts a `RawResponse` object into a `RawResponse` by extracting its properties,
39
+ * excluding the `body` and `bodyUsed` fields.
40
+ *
41
+ * @param response - The `RawResponse` object to convert.
42
+ * @returns A `RawResponse` object containing the extracted properties of the input response.
43
+ */
44
+ export function toRawResponse(response: Response): RawResponse {
45
+ return {
46
+ headers: response.headers,
47
+ redirected: response.redirected,
48
+ status: response.status,
49
+ statusText: response.statusText,
50
+ type: response.type,
51
+ url: response.url,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Creates a `RawResponse` from a standard `Response` object.
57
+ */
58
+ export interface WithRawResponse<T> {
59
+ readonly data: T;
60
+ readonly rawResponse: RawResponse;
61
+ }
@@ -0,0 +1,11 @@
1
+ export type Supplier<T> = T | Promise<T> | (() => T | Promise<T>);
2
+
3
+ export const Supplier = {
4
+ get: async <T>(supplier: Supplier<T>): Promise<T> => {
5
+ if (typeof supplier === "function") {
6
+ return (supplier as () => T)();
7
+ } else {
8
+ return supplier;
9
+ }
10
+ },
11
+ };
@@ -0,0 +1,6 @@
1
+ import { toQueryString } from "../url/qs.js";
2
+
3
+ export function createRequestUrl(baseUrl: string, queryParameters?: Record<string, unknown>): string {
4
+ const queryString = toQueryString(queryParameters, { arrayFormat: "repeat" });
5
+ return queryString ? `${baseUrl}?${queryString}` : baseUrl;
6
+ }
@@ -0,0 +1,33 @@
1
+ import { fromJson } from "../json.js";
2
+ import { getResponseBody } from "./getResponseBody.js";
3
+
4
+ export async function getErrorResponseBody(response: Response): Promise<unknown> {
5
+ let contentType = response.headers.get("Content-Type")?.toLowerCase();
6
+ if (contentType == null || contentType.length === 0) {
7
+ return getResponseBody(response);
8
+ }
9
+
10
+ if (contentType.indexOf(";") !== -1) {
11
+ contentType = contentType.split(";")[0]?.trim() ?? "";
12
+ }
13
+ switch (contentType) {
14
+ case "application/hal+json":
15
+ case "application/json":
16
+ case "application/ld+json":
17
+ case "application/problem+json":
18
+ case "application/vnd.api+json":
19
+ case "text/json": {
20
+ const text = await response.text();
21
+ return text.length > 0 ? fromJson(text) : undefined;
22
+ }
23
+ default:
24
+ if (contentType.startsWith("application/vnd.") && contentType.endsWith("+json")) {
25
+ const text = await response.text();
26
+ return text.length > 0 ? fromJson(text) : undefined;
27
+ }
28
+
29
+ // Fallback to plain text if content type is not recognized
30
+ // Even if no body is present, the response will be an empty string
31
+ return await response.text();
32
+ }
33
+ }
@@ -0,0 +1,3 @@
1
+ export async function getFetchFn(): Promise<typeof fetch> {
2
+ return fetch;
3
+ }
@@ -0,0 +1,8 @@
1
+ export function getHeader(headers: Record<string, any>, header: string): string | undefined {
2
+ for (const [headerKey, headerValue] of Object.entries(headers)) {
3
+ if (headerKey.toLowerCase() === header.toLowerCase()) {
4
+ return headerValue;
5
+ }
6
+ }
7
+ return undefined;
8
+ }
@@ -0,0 +1,20 @@
1
+ import { toJson } from "../json.js";
2
+ import { toQueryString } from "../url/qs.js";
3
+
4
+ export declare namespace GetRequestBody {
5
+ interface Args {
6
+ body: unknown;
7
+ type: "json" | "file" | "bytes" | "form" | "other";
8
+ }
9
+ }
10
+
11
+ export async function getRequestBody({ body, type }: GetRequestBody.Args): Promise<BodyInit | undefined> {
12
+ if (type === "form") {
13
+ return toQueryString(body, { arrayFormat: "repeat", encode: true });
14
+ }
15
+ if (type.includes("json")) {
16
+ return toJson(body);
17
+ } else {
18
+ return body as BodyInit;
19
+ }
20
+ }
@@ -0,0 +1,58 @@
1
+ import { fromJson } from "../json.js";
2
+ import { getBinaryResponse } from "./BinaryResponse.js";
3
+
4
+ export async function getResponseBody(response: Response, responseType?: string): Promise<unknown> {
5
+ switch (responseType) {
6
+ case "binary-response":
7
+ return getBinaryResponse(response);
8
+ case "blob":
9
+ return await response.blob();
10
+ case "arrayBuffer":
11
+ return await response.arrayBuffer();
12
+ case "sse":
13
+ if (response.body == null) {
14
+ return {
15
+ ok: false,
16
+ error: {
17
+ reason: "body-is-null",
18
+ statusCode: response.status,
19
+ },
20
+ };
21
+ }
22
+ return response.body;
23
+ case "streaming":
24
+ if (response.body == null) {
25
+ return {
26
+ ok: false,
27
+ error: {
28
+ reason: "body-is-null",
29
+ statusCode: response.status,
30
+ },
31
+ };
32
+ }
33
+
34
+ return response.body;
35
+
36
+ case "text":
37
+ return await response.text();
38
+ }
39
+
40
+ // if responseType is "json" or not specified, try to parse as JSON
41
+ const text = await response.text();
42
+ if (text.length > 0) {
43
+ try {
44
+ const responseBody = fromJson(text);
45
+ return responseBody;
46
+ } catch (_err) {
47
+ return {
48
+ ok: false,
49
+ error: {
50
+ reason: "non-json",
51
+ statusCode: response.status,
52
+ rawBody: text,
53
+ },
54
+ };
55
+ }
56
+ }
57
+ return undefined;
58
+ }
@@ -0,0 +1,11 @@
1
+ export type { APIResponse } from "./APIResponse.js";
2
+ export type { BinaryResponse } from "./BinaryResponse.js";
3
+ export type { EndpointMetadata } from "./EndpointMetadata.js";
4
+ export { EndpointSupplier } from "./EndpointSupplier.js";
5
+ export type { Fetcher, FetchFunction } from "./Fetcher.js";
6
+ export { fetcher } from "./Fetcher.js";
7
+ export { getHeader } from "./getHeader.js";
8
+ export { HttpResponsePromise } from "./HttpResponsePromise.js";
9
+ export type { RawResponse, WithRawResponse } from "./RawResponse.js";
10
+ export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js";
11
+ export { Supplier } from "./Supplier.js";
@@ -0,0 +1,42 @@
1
+ import { anySignal, getTimeoutSignal } from "./signals.js";
2
+
3
+ export const makeRequest = async (
4
+ fetchFn: (url: string, init: RequestInit) => Promise<Response>,
5
+ url: string,
6
+ method: string,
7
+ headers: Headers | Record<string, string>,
8
+ requestBody: BodyInit | undefined,
9
+ timeoutMs?: number,
10
+ abortSignal?: AbortSignal,
11
+ withCredentials?: boolean,
12
+ duplex?: "half",
13
+ ): Promise<Response> => {
14
+ const signals: AbortSignal[] = [];
15
+
16
+ let timeoutAbortId: ReturnType<typeof setTimeout> | undefined;
17
+ if (timeoutMs != null) {
18
+ const { signal, abortId } = getTimeoutSignal(timeoutMs);
19
+ timeoutAbortId = abortId;
20
+ signals.push(signal);
21
+ }
22
+
23
+ if (abortSignal != null) {
24
+ signals.push(abortSignal);
25
+ }
26
+ const newSignals = anySignal(signals);
27
+ const response = await fetchFn(url, {
28
+ method: method,
29
+ headers,
30
+ body: requestBody,
31
+ signal: newSignals,
32
+ credentials: withCredentials ? "include" : undefined,
33
+ // @ts-ignore
34
+ duplex,
35
+ });
36
+
37
+ if (timeoutAbortId != null) {
38
+ clearTimeout(timeoutAbortId);
39
+ }
40
+
41
+ return response;
42
+ };
@@ -0,0 +1,64 @@
1
+ const INITIAL_RETRY_DELAY = 1000; // in milliseconds
2
+ const MAX_RETRY_DELAY = 60000; // in milliseconds
3
+ const DEFAULT_MAX_RETRIES = 2;
4
+ const JITTER_FACTOR = 0.2; // 20% random jitter
5
+
6
+ function addPositiveJitter(delay: number): number {
7
+ const jitterMultiplier = 1 + Math.random() * JITTER_FACTOR;
8
+ return delay * jitterMultiplier;
9
+ }
10
+
11
+ function addSymmetricJitter(delay: number): number {
12
+ const jitterMultiplier = 1 + (Math.random() - 0.5) * JITTER_FACTOR;
13
+ return delay * jitterMultiplier;
14
+ }
15
+
16
+ function getRetryDelayFromHeaders(response: Response, retryAttempt: number): number {
17
+ const retryAfter = response.headers.get("Retry-After");
18
+ if (retryAfter) {
19
+ const retryAfterSeconds = parseInt(retryAfter, 10);
20
+ if (!Number.isNaN(retryAfterSeconds) && retryAfterSeconds > 0) {
21
+ return Math.min(retryAfterSeconds * 1000, MAX_RETRY_DELAY);
22
+ }
23
+
24
+ const retryAfterDate = new Date(retryAfter);
25
+ if (!Number.isNaN(retryAfterDate.getTime())) {
26
+ const delay = retryAfterDate.getTime() - Date.now();
27
+ if (delay > 0) {
28
+ return Math.min(Math.max(delay, 0), MAX_RETRY_DELAY);
29
+ }
30
+ }
31
+ }
32
+
33
+ const rateLimitReset = response.headers.get("X-RateLimit-Reset");
34
+ if (rateLimitReset) {
35
+ const resetTime = parseInt(rateLimitReset, 10);
36
+ if (!Number.isNaN(resetTime)) {
37
+ const delay = resetTime * 1000 - Date.now();
38
+ if (delay > 0) {
39
+ return addPositiveJitter(Math.min(delay, MAX_RETRY_DELAY));
40
+ }
41
+ }
42
+ }
43
+
44
+ return addSymmetricJitter(Math.min(INITIAL_RETRY_DELAY * 2 ** retryAttempt, MAX_RETRY_DELAY));
45
+ }
46
+
47
+ export async function requestWithRetries(
48
+ requestFn: () => Promise<Response>,
49
+ maxRetries: number = DEFAULT_MAX_RETRIES,
50
+ ): Promise<Response> {
51
+ let response: Response = await requestFn();
52
+
53
+ for (let i = 0; i < maxRetries; ++i) {
54
+ if ([408, 429].includes(response.status) || response.status >= 500) {
55
+ const delay = getRetryDelayFromHeaders(response, i);
56
+
57
+ await new Promise((resolve) => setTimeout(resolve, delay));
58
+ response = await requestFn();
59
+ } else {
60
+ break;
61
+ }
62
+ }
63
+ return response!;
64
+ }
@@ -0,0 +1,26 @@
1
+ const TIMEOUT = "timeout";
2
+
3
+ export function getTimeoutSignal(timeoutMs: number): { signal: AbortSignal; abortId: ReturnType<typeof setTimeout> } {
4
+ const controller = new AbortController();
5
+ const abortId = setTimeout(() => controller.abort(TIMEOUT), timeoutMs);
6
+ return { signal: controller.signal, abortId };
7
+ }
8
+
9
+ export function anySignal(...args: AbortSignal[] | [AbortSignal[]]): AbortSignal {
10
+ const signals = (args.length === 1 && Array.isArray(args[0]) ? args[0] : args) as AbortSignal[];
11
+
12
+ const controller = new AbortController();
13
+
14
+ for (const signal of signals) {
15
+ if (signal.aborted) {
16
+ controller.abort((signal as any)?.reason);
17
+ break;
18
+ }
19
+
20
+ signal.addEventListener("abort", () => controller.abort((signal as any)?.reason), {
21
+ signal: controller.signal,
22
+ });
23
+ }
24
+
25
+ return controller.signal;
26
+ }
@@ -0,0 +1 @@
1
+ export type { Uploadable } from "./types.js";
@@ -0,0 +1,217 @@
1
+ import type { Uploadable } from "./types.js";
2
+
3
+ export async function toBinaryUploadRequest(
4
+ file: Uploadable,
5
+ ): Promise<{ body: Uploadable.FileLike; headers?: Record<string, string> }> {
6
+ const { data, filename, contentLength, contentType } = await getFileWithMetadata(file);
7
+ const request = {
8
+ body: data,
9
+ headers: {} as Record<string, string>,
10
+ };
11
+ if (filename) {
12
+ request.headers["Content-Disposition"] = `attachment; filename="${filename}"`;
13
+ }
14
+ if (contentType) {
15
+ request.headers["Content-Type"] = contentType;
16
+ }
17
+ if (contentLength != null) {
18
+ request.headers["Content-Length"] = contentLength.toString();
19
+ }
20
+ return request;
21
+ }
22
+
23
+ export async function toMultipartDataPart(
24
+ file: Uploadable,
25
+ ): Promise<{ data: Uploadable.FileLike; filename?: string; contentType?: string }> {
26
+ const { data, filename, contentType } = await getFileWithMetadata(file, {
27
+ noSniffFileSize: true,
28
+ });
29
+ return {
30
+ data,
31
+ filename,
32
+ contentType,
33
+ };
34
+ }
35
+
36
+ async function getFileWithMetadata(
37
+ file: Uploadable,
38
+ { noSniffFileSize }: { noSniffFileSize?: boolean } = {},
39
+ ): Promise<Uploadable.WithMetadata> {
40
+ if (isFileLike(file)) {
41
+ return getFileWithMetadata(
42
+ {
43
+ data: file,
44
+ },
45
+ { noSniffFileSize },
46
+ );
47
+ }
48
+
49
+ if ("path" in file) {
50
+ const fs = await import("fs");
51
+ if (!fs || !fs.createReadStream) {
52
+ throw new Error("File path uploads are not supported in this environment.");
53
+ }
54
+ const data = fs.createReadStream(file.path);
55
+ const contentLength =
56
+ file.contentLength ?? (noSniffFileSize === true ? undefined : await tryGetFileSizeFromPath(file.path));
57
+ const filename = file.filename ?? getNameFromPath(file.path);
58
+ return {
59
+ data,
60
+ filename,
61
+ contentType: file.contentType,
62
+ contentLength,
63
+ };
64
+ }
65
+ if ("data" in file) {
66
+ const data = file.data;
67
+ const contentLength =
68
+ file.contentLength ??
69
+ (await tryGetContentLengthFromFileLike(data, {
70
+ noSniffFileSize,
71
+ }));
72
+ const filename = file.filename ?? tryGetNameFromFileLike(data);
73
+ return {
74
+ data,
75
+ filename,
76
+ contentType: file.contentType ?? tryGetContentTypeFromFileLike(data),
77
+ contentLength,
78
+ };
79
+ }
80
+
81
+ throw new Error(`Invalid FileUpload of type ${typeof file}: ${JSON.stringify(file)}`);
82
+ }
83
+
84
+ function isFileLike(value: unknown): value is Uploadable.FileLike {
85
+ return (
86
+ isBuffer(value) ||
87
+ isArrayBufferView(value) ||
88
+ isArrayBuffer(value) ||
89
+ isUint8Array(value) ||
90
+ isBlob(value) ||
91
+ isFile(value) ||
92
+ isStreamLike(value) ||
93
+ isReadableStream(value)
94
+ );
95
+ }
96
+
97
+ async function tryGetFileSizeFromPath(path: string): Promise<number | undefined> {
98
+ try {
99
+ const fs = await import("fs");
100
+ if (!fs || !fs.promises || !fs.promises.stat) {
101
+ return undefined;
102
+ }
103
+ const fileStat = await fs.promises.stat(path);
104
+ return fileStat.size;
105
+ } catch (_fallbackError) {
106
+ return undefined;
107
+ }
108
+ }
109
+
110
+ function tryGetNameFromFileLike(data: Uploadable.FileLike): string | undefined {
111
+ if (isNamedValue(data)) {
112
+ return data.name;
113
+ }
114
+ if (isPathedValue(data)) {
115
+ return getNameFromPath(data.path.toString());
116
+ }
117
+ return undefined;
118
+ }
119
+
120
+ async function tryGetContentLengthFromFileLike(
121
+ data: Uploadable.FileLike,
122
+ { noSniffFileSize }: { noSniffFileSize?: boolean } = {},
123
+ ): Promise<number | undefined> {
124
+ if (isBuffer(data)) {
125
+ return data.length;
126
+ }
127
+ if (isArrayBufferView(data)) {
128
+ return data.byteLength;
129
+ }
130
+ if (isArrayBuffer(data)) {
131
+ return data.byteLength;
132
+ }
133
+ if (isBlob(data)) {
134
+ return data.size;
135
+ }
136
+ if (isFile(data)) {
137
+ return data.size;
138
+ }
139
+ if (noSniffFileSize === true) {
140
+ return undefined;
141
+ }
142
+ if (isPathedValue(data)) {
143
+ return await tryGetFileSizeFromPath(data.path.toString());
144
+ }
145
+ return undefined;
146
+ }
147
+
148
+ function tryGetContentTypeFromFileLike(data: Uploadable.FileLike): string | undefined {
149
+ if (isBlob(data)) {
150
+ return data.type;
151
+ }
152
+ if (isFile(data)) {
153
+ return data.type;
154
+ }
155
+
156
+ return undefined;
157
+ }
158
+
159
+ function getNameFromPath(path: string): string | undefined {
160
+ const lastForwardSlash = path.lastIndexOf("/");
161
+ const lastBackSlash = path.lastIndexOf("\\");
162
+ const lastSlashIndex = Math.max(lastForwardSlash, lastBackSlash);
163
+ return lastSlashIndex >= 0 ? path.substring(lastSlashIndex + 1) : path;
164
+ }
165
+
166
+ type NamedValue = {
167
+ name: string;
168
+ } & unknown;
169
+
170
+ type PathedValue = {
171
+ path: string | { toString(): string };
172
+ } & unknown;
173
+
174
+ type StreamLike = {
175
+ read?: () => unknown;
176
+ pipe?: (dest: unknown) => unknown;
177
+ } & unknown;
178
+
179
+ function isNamedValue(value: unknown): value is NamedValue {
180
+ return typeof value === "object" && value != null && "name" in value;
181
+ }
182
+
183
+ function isPathedValue(value: unknown): value is PathedValue {
184
+ return typeof value === "object" && value != null && "path" in value;
185
+ }
186
+
187
+ function isStreamLike(value: unknown): value is StreamLike {
188
+ return typeof value === "object" && value != null && ("read" in value || "pipe" in value);
189
+ }
190
+
191
+ function isReadableStream(value: unknown): value is ReadableStream {
192
+ return typeof value === "object" && value != null && "getReader" in value;
193
+ }
194
+
195
+ function isBuffer(value: unknown): value is Buffer {
196
+ return typeof Buffer !== "undefined" && Buffer.isBuffer && Buffer.isBuffer(value);
197
+ }
198
+
199
+ function isArrayBufferView(value: unknown): value is ArrayBufferView {
200
+ return typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(value);
201
+ }
202
+
203
+ function isArrayBuffer(value: unknown): value is ArrayBuffer {
204
+ return typeof ArrayBuffer !== "undefined" && value instanceof ArrayBuffer;
205
+ }
206
+
207
+ function isUint8Array(value: unknown): value is Uint8Array {
208
+ return typeof Uint8Array !== "undefined" && value instanceof Uint8Array;
209
+ }
210
+
211
+ function isBlob(value: unknown): value is Blob {
212
+ return typeof Blob !== "undefined" && value instanceof Blob;
213
+ }
214
+
215
+ function isFile(value: unknown): value is File {
216
+ return typeof File !== "undefined" && value instanceof File;
217
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./file.js";
2
+ export * from "./types.js";
@@ -0,0 +1,81 @@
1
+ /**
2
+ * A file that can be uploaded. Can be a file-like object (stream, buffer, blob, etc.),
3
+ * a path to a file, or an object with a file-like object and metadata.
4
+ */
5
+ export type Uploadable = Uploadable.FileLike | Uploadable.FromPath | Uploadable.WithMetadata;
6
+
7
+ export namespace Uploadable {
8
+ /**
9
+ * Various file-like objects that can be used to upload a file.
10
+ */
11
+ export type FileLike =
12
+ | ArrayBuffer
13
+ | ArrayBufferLike
14
+ | ArrayBufferView
15
+ | Uint8Array
16
+ | import("buffer").Buffer
17
+ | import("buffer").Blob
18
+ | import("buffer").File
19
+ | import("stream").Readable
20
+ | import("stream/web").ReadableStream
21
+ | globalThis.Blob
22
+ | globalThis.File
23
+ | ReadableStream;
24
+
25
+ /**
26
+ * A file path with optional metadata, used for uploading a file from the file system.
27
+ */
28
+ export type FromPath = {
29
+ /** The path to the file to upload */
30
+ path: string;
31
+ /**
32
+ * Optional override for the file name (defaults to basename of path).
33
+ * This is used to set the `Content-Disposition` header in upload requests.
34
+ */
35
+ filename?: string;
36
+ /**
37
+ * Optional MIME type of the file (e.g., 'image/jpeg', 'text/plain').
38
+ * This is used to set the `Content-Type` header in upload requests.
39
+ */
40
+ contentType?: string;
41
+ /**
42
+ * Optional file size in bytes.
43
+ * If not provided, the file size will be determined from the file system.
44
+ * The content length is used to set the `Content-Length` header in upload requests.
45
+ */
46
+ contentLength?: number;
47
+ };
48
+
49
+ /**
50
+ * A file-like object with metadata, used for uploading files.
51
+ */
52
+ export type WithMetadata = {
53
+ /** The file data */
54
+ data: FileLike;
55
+ /**
56
+ * Optional override for the file name (defaults to basename of path).
57
+ * This is used to set the `Content-Disposition` header in upload requests.
58
+ */
59
+ filename?: string;
60
+ /**
61
+ * Optional MIME type of the file (e.g., 'image/jpeg', 'text/plain').
62
+ * This is used to set the `Content-Type` header in upload requests.
63
+ *
64
+ * If not provided, the content type may be determined from the data itself.
65
+ * * If the data is a `File`, `Blob`, or similar, the content type will be determined from the file itself, if the type is set.
66
+ * * Any other data type will not have a content type set, and the upload request will use `Content-Type: application/octet-stream` instead.
67
+ */
68
+ contentType?: string;
69
+ /**
70
+ * Optional file size in bytes.
71
+ * The content length is used to set the `Content-Length` header in upload requests.
72
+ * If the content length is not provided and cannot be determined, the upload request will not include the `Content-Length` header, but will use `Transfer-Encoding: chunked` instead.
73
+ *
74
+ * If not provided, the file size will be determined depending on the data type.
75
+ * * If the data is of type `fs.ReadStream` (`createReadStream`), the size will be determined from the file system.
76
+ * * If the data is a `Buffer`, `ArrayBuffer`, `Uint8Array`, `Blob`, `File`, or similar, the size will be determined from the data itself.
77
+ * * If the data is a `Readable` or `ReadableStream`, the size will not be determined.
78
+ */
79
+ contentLength?: number;
80
+ };
81
+ }