@hyperserve/hyperserve-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,176 @@
1
+ type VideoResolution = "144p" | "240p" | "360p" | "480p" | "720p" | "1080p" | "1440p" | "4k" | "8k";
2
+ type VideoStatus = "pending_upload" | "processing" | "ready" | "fail";
3
+ interface HyperserveClientOptions {
4
+ /** Your Hyperserve API key. Must be kept server-side — never expose in browser code. */
5
+ apiKey: string;
6
+ /** Override the base URL (must include the /api prefix). Useful for local development. Defaults to https://api.hyperserve.io/api */
7
+ baseUrl?: string;
8
+ /** Timeout in milliseconds for API calls (not the storage PUT). Defaults to 30000. */
9
+ timeoutMs?: number;
10
+ /**
11
+ * Number of additional retry attempts on transient failures (5xx responses and network errors).
12
+ * Uses exponential backoff with full jitter (random delay up to min(10s, 100ms × 2^attempt)).
13
+ * Does not retry on 4xx errors or timeouts. Defaults to 0 (no retries).
14
+ */
15
+ retries?: number;
16
+ }
17
+ interface VerifyWebhookSignatureOptions {
18
+ /**
19
+ * Value of the x-hyperserve-signature header from the incoming webhook request.
20
+ * Format: "{timestampMs}.{hmac-sha256-hex}"
21
+ */
22
+ signature: string;
23
+ /** Your webhook signing secret from the Hyperserve dashboard. */
24
+ secret: string;
25
+ /**
26
+ * The raw request body as a string. Must be the exact bytes received — do not parse
27
+ * and re-serialize, as any whitespace difference will invalidate the signature.
28
+ *
29
+ * @example
30
+ * // Express
31
+ * app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
32
+ * const body = req.body.toString();
33
+ * ...
34
+ * });
35
+ *
36
+ * // Next.js App Router
37
+ * const body = await request.text();
38
+ */
39
+ body: string;
40
+ /**
41
+ * Maximum age of the timestamp in milliseconds. Defaults to 300000 (5 minutes).
42
+ * Must match or exceed the server-side tolerance to avoid rejecting valid webhooks.
43
+ */
44
+ toleranceMs?: number;
45
+ }
46
+ interface CreateVideoOptions {
47
+ /** Original filename including extension (e.g. "promo.mp4"). Used server-side to derive content type. */
48
+ filename: string;
49
+ /** File size in bytes. */
50
+ fileSizeBytes: number;
51
+ /** At least one resolution is required. */
52
+ resolutions: [VideoResolution, ...VideoResolution[]];
53
+ /** Controls whether playback URLs are public or time-limited signed URLs. */
54
+ isPublic: boolean;
55
+ /** Timestamps (seconds) at which to generate thumbnail images. */
56
+ thumbnailTimestampsSeconds?: number[];
57
+ /** Arbitrary key/value stored against the video. */
58
+ customMetadata?: Record<string, unknown>;
59
+ }
60
+ interface CreateVideoResult {
61
+ /** Video ID — use in completeUpload and all subsequent calls. */
62
+ id: string;
63
+ /** Presigned PUT URL for the original file. Pass to your frontend. Expires shortly. */
64
+ uploadUrl: string;
65
+ /** Exact Content-Type to send on the presigned PUT. Pass to your frontend alongside uploadUrl. */
66
+ contentType: string;
67
+ isPublic: boolean;
68
+ resolutions: Record<VideoResolution, {
69
+ status: "pending_upload";
70
+ }>;
71
+ }
72
+ interface CompleteUploadResult {
73
+ id: string;
74
+ isPublic: boolean;
75
+ resolutions: Record<VideoResolution, {
76
+ status: VideoStatus;
77
+ }>;
78
+ }
79
+ interface UploadVideoOptions {
80
+ /** The video file. Use File/Blob in browser contexts, Buffer or ReadableStream in Node. */
81
+ file: Blob | Buffer | ReadableStream;
82
+ /** Filename including extension (e.g. "clip.mp4"). */
83
+ filename: string;
84
+ /** Required when file is a ReadableStream (cannot be inferred). Inferred automatically for Blob/Buffer. */
85
+ fileSizeBytes?: number;
86
+ resolutions: [VideoResolution, ...VideoResolution[]];
87
+ isPublic: boolean;
88
+ thumbnailTimestampsSeconds?: number[];
89
+ customMetadata?: Record<string, unknown>;
90
+ }
91
+ interface GetVideoOptions {
92
+ /** Return time-limited signed URLs instead of public URLs. */
93
+ private?: boolean;
94
+ /** Signed URL TTL in seconds when private is true. Defaults to 3600. */
95
+ expirationSeconds?: number;
96
+ }
97
+ interface VideoResolutionResult {
98
+ id: string;
99
+ status: VideoStatus;
100
+ videoUrl: string;
101
+ thumbnailImageUrls: string[];
102
+ }
103
+ interface VideoResult {
104
+ id: string;
105
+ status: VideoStatus;
106
+ isPublic: boolean;
107
+ resolutions: Partial<Record<VideoResolution, VideoResolutionResult>>;
108
+ }
109
+ interface PutVideoToStorageOptions {
110
+ /** Presigned PUT URL obtained from your backend (which called createVideo). */
111
+ uploadUrl: string;
112
+ /** Content-Type obtained from your backend alongside uploadUrl. */
113
+ contentType: string;
114
+ /** The video file from a browser file picker or drag-and-drop. */
115
+ file: File | Blob;
116
+ /** Called during the upload with progress 0–100. Uses XHR when provided. */
117
+ onProgress?: (percent: number) => void;
118
+ }
119
+ interface PutVideoToStorageRNOptions {
120
+ /** Presigned PUT URL obtained from your backend (which called createVideo). */
121
+ uploadUrl: string;
122
+ /** Content-Type obtained from your backend alongside uploadUrl. */
123
+ contentType: string;
124
+ /**
125
+ * Local file URI from a React Native video/image picker.
126
+ * e.g. "file:///var/mobile/Containers/.../video.mp4"
127
+ * Accepted from expo-image-picker, react-native-image-picker, expo-document-picker, etc.
128
+ */
129
+ uri: string;
130
+ /** Called during the upload with progress 0–100. Uses XHR when provided. */
131
+ onProgress?: (percent: number) => void;
132
+ }
133
+
134
+ /**
135
+ * Base class for all Hyperserve SDK errors.
136
+ */
137
+ declare class HyperserveError extends Error {
138
+ readonly statusCode?: number | undefined;
139
+ constructor(message: string, statusCode?: number | undefined);
140
+ }
141
+ /**
142
+ * The Hyperserve API returned a 4xx response.
143
+ * Typically indicates a validation problem: unsupported file format,
144
+ * file too large, invalid resolutions, video not in expected state, etc.
145
+ */
146
+ declare class HyperserveValidationError extends HyperserveError {
147
+ readonly detail?: unknown | undefined;
148
+ constructor(message: string, statusCode: number, detail?: unknown | undefined);
149
+ }
150
+ /**
151
+ * The Hyperserve API returned a 404 response.
152
+ */
153
+ declare class HyperserveNotFoundError extends HyperserveError {
154
+ constructor(message?: string);
155
+ }
156
+ /**
157
+ * The Hyperserve API returned a 5xx response.
158
+ */
159
+ declare class HyperserveApiError extends HyperserveError {
160
+ constructor(message: string, statusCode: number);
161
+ }
162
+ /**
163
+ * The storage PUT request failed.
164
+ */
165
+ declare class HyperserveUploadError extends HyperserveError {
166
+ readonly uploadStatus?: number | undefined;
167
+ constructor(message: string, uploadStatus?: number | undefined);
168
+ }
169
+ /**
170
+ * A request exceeded the configured timeoutMs.
171
+ */
172
+ declare class HyperserveTimeoutError extends HyperserveError {
173
+ constructor(message?: string);
174
+ }
175
+
176
+ export { type CreateVideoOptions as C, type GetVideoOptions as G, type HyperserveClientOptions as H, type PutVideoToStorageOptions as P, type UploadVideoOptions as U, type VideoResult as V, type CreateVideoResult as a, type CompleteUploadResult as b, type VerifyWebhookSignatureOptions as c, HyperserveApiError as d, HyperserveError as e, HyperserveNotFoundError as f, HyperserveTimeoutError as g, HyperserveUploadError as h, HyperserveValidationError as i, type PutVideoToStorageRNOptions as j, type VideoResolution as k, type VideoResolutionResult as l, type VideoStatus as m };
@@ -0,0 +1,176 @@
1
+ type VideoResolution = "144p" | "240p" | "360p" | "480p" | "720p" | "1080p" | "1440p" | "4k" | "8k";
2
+ type VideoStatus = "pending_upload" | "processing" | "ready" | "fail";
3
+ interface HyperserveClientOptions {
4
+ /** Your Hyperserve API key. Must be kept server-side — never expose in browser code. */
5
+ apiKey: string;
6
+ /** Override the base URL (must include the /api prefix). Useful for local development. Defaults to https://api.hyperserve.io/api */
7
+ baseUrl?: string;
8
+ /** Timeout in milliseconds for API calls (not the storage PUT). Defaults to 30000. */
9
+ timeoutMs?: number;
10
+ /**
11
+ * Number of additional retry attempts on transient failures (5xx responses and network errors).
12
+ * Uses exponential backoff with full jitter (random delay up to min(10s, 100ms × 2^attempt)).
13
+ * Does not retry on 4xx errors or timeouts. Defaults to 0 (no retries).
14
+ */
15
+ retries?: number;
16
+ }
17
+ interface VerifyWebhookSignatureOptions {
18
+ /**
19
+ * Value of the x-hyperserve-signature header from the incoming webhook request.
20
+ * Format: "{timestampMs}.{hmac-sha256-hex}"
21
+ */
22
+ signature: string;
23
+ /** Your webhook signing secret from the Hyperserve dashboard. */
24
+ secret: string;
25
+ /**
26
+ * The raw request body as a string. Must be the exact bytes received — do not parse
27
+ * and re-serialize, as any whitespace difference will invalidate the signature.
28
+ *
29
+ * @example
30
+ * // Express
31
+ * app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
32
+ * const body = req.body.toString();
33
+ * ...
34
+ * });
35
+ *
36
+ * // Next.js App Router
37
+ * const body = await request.text();
38
+ */
39
+ body: string;
40
+ /**
41
+ * Maximum age of the timestamp in milliseconds. Defaults to 300000 (5 minutes).
42
+ * Must match or exceed the server-side tolerance to avoid rejecting valid webhooks.
43
+ */
44
+ toleranceMs?: number;
45
+ }
46
+ interface CreateVideoOptions {
47
+ /** Original filename including extension (e.g. "promo.mp4"). Used server-side to derive content type. */
48
+ filename: string;
49
+ /** File size in bytes. */
50
+ fileSizeBytes: number;
51
+ /** At least one resolution is required. */
52
+ resolutions: [VideoResolution, ...VideoResolution[]];
53
+ /** Controls whether playback URLs are public or time-limited signed URLs. */
54
+ isPublic: boolean;
55
+ /** Timestamps (seconds) at which to generate thumbnail images. */
56
+ thumbnailTimestampsSeconds?: number[];
57
+ /** Arbitrary key/value stored against the video. */
58
+ customMetadata?: Record<string, unknown>;
59
+ }
60
+ interface CreateVideoResult {
61
+ /** Video ID — use in completeUpload and all subsequent calls. */
62
+ id: string;
63
+ /** Presigned PUT URL for the original file. Pass to your frontend. Expires shortly. */
64
+ uploadUrl: string;
65
+ /** Exact Content-Type to send on the presigned PUT. Pass to your frontend alongside uploadUrl. */
66
+ contentType: string;
67
+ isPublic: boolean;
68
+ resolutions: Record<VideoResolution, {
69
+ status: "pending_upload";
70
+ }>;
71
+ }
72
+ interface CompleteUploadResult {
73
+ id: string;
74
+ isPublic: boolean;
75
+ resolutions: Record<VideoResolution, {
76
+ status: VideoStatus;
77
+ }>;
78
+ }
79
+ interface UploadVideoOptions {
80
+ /** The video file. Use File/Blob in browser contexts, Buffer or ReadableStream in Node. */
81
+ file: Blob | Buffer | ReadableStream;
82
+ /** Filename including extension (e.g. "clip.mp4"). */
83
+ filename: string;
84
+ /** Required when file is a ReadableStream (cannot be inferred). Inferred automatically for Blob/Buffer. */
85
+ fileSizeBytes?: number;
86
+ resolutions: [VideoResolution, ...VideoResolution[]];
87
+ isPublic: boolean;
88
+ thumbnailTimestampsSeconds?: number[];
89
+ customMetadata?: Record<string, unknown>;
90
+ }
91
+ interface GetVideoOptions {
92
+ /** Return time-limited signed URLs instead of public URLs. */
93
+ private?: boolean;
94
+ /** Signed URL TTL in seconds when private is true. Defaults to 3600. */
95
+ expirationSeconds?: number;
96
+ }
97
+ interface VideoResolutionResult {
98
+ id: string;
99
+ status: VideoStatus;
100
+ videoUrl: string;
101
+ thumbnailImageUrls: string[];
102
+ }
103
+ interface VideoResult {
104
+ id: string;
105
+ status: VideoStatus;
106
+ isPublic: boolean;
107
+ resolutions: Partial<Record<VideoResolution, VideoResolutionResult>>;
108
+ }
109
+ interface PutVideoToStorageOptions {
110
+ /** Presigned PUT URL obtained from your backend (which called createVideo). */
111
+ uploadUrl: string;
112
+ /** Content-Type obtained from your backend alongside uploadUrl. */
113
+ contentType: string;
114
+ /** The video file from a browser file picker or drag-and-drop. */
115
+ file: File | Blob;
116
+ /** Called during the upload with progress 0–100. Uses XHR when provided. */
117
+ onProgress?: (percent: number) => void;
118
+ }
119
+ interface PutVideoToStorageRNOptions {
120
+ /** Presigned PUT URL obtained from your backend (which called createVideo). */
121
+ uploadUrl: string;
122
+ /** Content-Type obtained from your backend alongside uploadUrl. */
123
+ contentType: string;
124
+ /**
125
+ * Local file URI from a React Native video/image picker.
126
+ * e.g. "file:///var/mobile/Containers/.../video.mp4"
127
+ * Accepted from expo-image-picker, react-native-image-picker, expo-document-picker, etc.
128
+ */
129
+ uri: string;
130
+ /** Called during the upload with progress 0–100. Uses XHR when provided. */
131
+ onProgress?: (percent: number) => void;
132
+ }
133
+
134
+ /**
135
+ * Base class for all Hyperserve SDK errors.
136
+ */
137
+ declare class HyperserveError extends Error {
138
+ readonly statusCode?: number | undefined;
139
+ constructor(message: string, statusCode?: number | undefined);
140
+ }
141
+ /**
142
+ * The Hyperserve API returned a 4xx response.
143
+ * Typically indicates a validation problem: unsupported file format,
144
+ * file too large, invalid resolutions, video not in expected state, etc.
145
+ */
146
+ declare class HyperserveValidationError extends HyperserveError {
147
+ readonly detail?: unknown | undefined;
148
+ constructor(message: string, statusCode: number, detail?: unknown | undefined);
149
+ }
150
+ /**
151
+ * The Hyperserve API returned a 404 response.
152
+ */
153
+ declare class HyperserveNotFoundError extends HyperserveError {
154
+ constructor(message?: string);
155
+ }
156
+ /**
157
+ * The Hyperserve API returned a 5xx response.
158
+ */
159
+ declare class HyperserveApiError extends HyperserveError {
160
+ constructor(message: string, statusCode: number);
161
+ }
162
+ /**
163
+ * The storage PUT request failed.
164
+ */
165
+ declare class HyperserveUploadError extends HyperserveError {
166
+ readonly uploadStatus?: number | undefined;
167
+ constructor(message: string, uploadStatus?: number | undefined);
168
+ }
169
+ /**
170
+ * A request exceeded the configured timeoutMs.
171
+ */
172
+ declare class HyperserveTimeoutError extends HyperserveError {
173
+ constructor(message?: string);
174
+ }
175
+
176
+ export { type CreateVideoOptions as C, type GetVideoOptions as G, type HyperserveClientOptions as H, type PutVideoToStorageOptions as P, type UploadVideoOptions as U, type VideoResult as V, type CreateVideoResult as a, type CompleteUploadResult as b, type VerifyWebhookSignatureOptions as c, HyperserveApiError as d, HyperserveError as e, HyperserveNotFoundError as f, HyperserveTimeoutError as g, HyperserveUploadError as h, HyperserveValidationError as i, type PutVideoToStorageRNOptions as j, type VideoResolution as k, type VideoResolutionResult as l, type VideoStatus as m };
package/dist/index.cjs ADDED
@@ -0,0 +1,342 @@
1
+ 'use strict';
2
+
3
+ // src/errors.ts
4
+ var HyperserveError = class extends Error {
5
+ constructor(message, statusCode) {
6
+ super(message);
7
+ this.statusCode = statusCode;
8
+ this.name = "HyperserveError";
9
+ Object.setPrototypeOf(this, new.target.prototype);
10
+ }
11
+ };
12
+ var HyperserveValidationError = class extends HyperserveError {
13
+ constructor(message, statusCode, detail) {
14
+ super(message, statusCode);
15
+ this.detail = detail;
16
+ this.name = "HyperserveValidationError";
17
+ Object.setPrototypeOf(this, new.target.prototype);
18
+ }
19
+ };
20
+ var HyperserveNotFoundError = class extends HyperserveError {
21
+ constructor(message = "Resource not found") {
22
+ super(message, 404);
23
+ this.name = "HyperserveNotFoundError";
24
+ Object.setPrototypeOf(this, new.target.prototype);
25
+ }
26
+ };
27
+ var HyperserveApiError = class extends HyperserveError {
28
+ constructor(message, statusCode) {
29
+ super(message, statusCode);
30
+ this.name = "HyperserveApiError";
31
+ Object.setPrototypeOf(this, new.target.prototype);
32
+ }
33
+ };
34
+ var HyperserveUploadError = class extends HyperserveError {
35
+ constructor(message, uploadStatus) {
36
+ super(message);
37
+ this.uploadStatus = uploadStatus;
38
+ this.name = "HyperserveUploadError";
39
+ Object.setPrototypeOf(this, new.target.prototype);
40
+ }
41
+ };
42
+ var HyperserveTimeoutError = class extends HyperserveError {
43
+ constructor(message = "Request timed out") {
44
+ super(message);
45
+ this.name = "HyperserveTimeoutError";
46
+ Object.setPrototypeOf(this, new.target.prototype);
47
+ }
48
+ };
49
+
50
+ // src/http.ts
51
+ function sleep(ms) {
52
+ return new Promise((resolve) => setTimeout(resolve, ms));
53
+ }
54
+ function isRetryable(err) {
55
+ if (err instanceof HyperserveApiError && err.statusCode !== void 0 && err.statusCode >= 500)
56
+ return true;
57
+ if (err instanceof Error && !(err instanceof HyperserveError)) return true;
58
+ return false;
59
+ }
60
+ async function apiRequest(options) {
61
+ const { retries = 0 } = options;
62
+ let attempt = 0;
63
+ while (true) {
64
+ try {
65
+ return await attemptRequest(options);
66
+ } catch (err) {
67
+ if (attempt >= retries || !isRetryable(err)) {
68
+ throw err;
69
+ }
70
+ const delay = Math.random() * Math.min(1e4, 100 * 2 ** attempt);
71
+ await sleep(delay);
72
+ attempt++;
73
+ }
74
+ }
75
+ }
76
+ async function attemptRequest(options) {
77
+ const { method, url, apiKey, timeoutMs, body } = options;
78
+ const controller = new AbortController();
79
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
80
+ let response;
81
+ try {
82
+ response = await fetch(url, {
83
+ method,
84
+ headers: {
85
+ "X-API-KEY": apiKey,
86
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {}
87
+ },
88
+ // Omit body entirely when not present — passing body: null on DELETE requests
89
+ // can be treated differently by some proxies and intermediaries.
90
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {},
91
+ signal: controller.signal
92
+ });
93
+ } catch (err) {
94
+ if (err instanceof Error && err.name === "AbortError") {
95
+ throw new HyperserveTimeoutError(`Request to ${url} timed out after ${timeoutMs}ms`);
96
+ }
97
+ throw err;
98
+ } finally {
99
+ clearTimeout(timer);
100
+ }
101
+ if (response.ok) {
102
+ if (response.status === 204) {
103
+ return void 0;
104
+ }
105
+ try {
106
+ return await response.json();
107
+ } catch {
108
+ throw new HyperserveApiError(`Failed to parse response from ${url}`, response.status);
109
+ }
110
+ }
111
+ let errorBody = {};
112
+ try {
113
+ errorBody = await response.json();
114
+ } catch {
115
+ }
116
+ const message = errorBody.message ?? response.statusText;
117
+ if (response.status === 404) {
118
+ throw new HyperserveNotFoundError(message);
119
+ }
120
+ if (response.status >= 400 && response.status < 500) {
121
+ throw new HyperserveValidationError(message, response.status, errorBody);
122
+ }
123
+ throw new HyperserveApiError(message, response.status);
124
+ }
125
+
126
+ // src/normalize.ts
127
+ function normalizeFile(file, filename, fileSizeBytes) {
128
+ if (file instanceof ReadableStream) {
129
+ if (fileSizeBytes === void 0) {
130
+ throw new TypeError(
131
+ "fileSizeBytes is required when file is a ReadableStream (size cannot be inferred)"
132
+ );
133
+ }
134
+ return { body: file, size: fileSizeBytes };
135
+ }
136
+ if (Buffer.isBuffer(file)) {
137
+ const size2 = fileSizeBytes ?? file.byteLength;
138
+ const blob = new Blob([new Uint8Array(file)], { type: deriveTypeHint(filename) });
139
+ return { body: blob, size: size2 };
140
+ }
141
+ const size = fileSizeBytes ?? file.size;
142
+ return { body: file, size };
143
+ }
144
+ function deriveTypeHint(filename) {
145
+ const ext = filename.slice(filename.lastIndexOf(".") + 1).toLowerCase();
146
+ const map = {
147
+ mp4: "video/mp4",
148
+ mov: "video/quicktime",
149
+ webm: "video/webm",
150
+ avi: "video/x-msvideo",
151
+ mkv: "video/x-matroska",
152
+ m4v: "video/x-m4v"
153
+ };
154
+ return map[ext] ?? "application/octet-stream";
155
+ }
156
+
157
+ // src/storage.ts
158
+ async function putToStorage(uploadUrl, contentType, body, onProgress) {
159
+ return putWithFetch(uploadUrl, contentType, body);
160
+ }
161
+ function putWithFetch(uploadUrl, contentType, body) {
162
+ return fetch(uploadUrl, {
163
+ method: "PUT",
164
+ headers: { "Content-Type": contentType },
165
+ // duplex is required for ReadableStream bodies in some runtimes (Node 18)
166
+ ...body instanceof ReadableStream ? { duplex: "half" } : {},
167
+ body
168
+ }).then((response) => {
169
+ if (!response.ok) {
170
+ throw new HyperserveUploadError(
171
+ `Storage PUT failed with status ${response.status}`,
172
+ response.status
173
+ );
174
+ }
175
+ });
176
+ }
177
+
178
+ // src/client.ts
179
+ var DEFAULT_BASE_URL = "https://api.hyperserve.io/api";
180
+ var DEFAULT_TIMEOUT_MS = 3e4;
181
+ var HyperserveClient = class {
182
+ constructor(options) {
183
+ this.apiKey = options.apiKey;
184
+ this.baseUrl = options.baseUrl?.replace(/\/$/, "") ?? DEFAULT_BASE_URL;
185
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
186
+ this.retries = options.retries ?? 0;
187
+ }
188
+ /**
189
+ * Creates a video record and returns a presigned upload URL.
190
+ * Pass uploadUrl and contentType to your frontend so it can PUT the file directly to storage.
191
+ * Call completeUpload once the frontend confirms the PUT is done.
192
+ */
193
+ async createVideo(options) {
194
+ return apiRequest({
195
+ method: "POST",
196
+ url: `${this.baseUrl}/video`,
197
+ apiKey: this.apiKey,
198
+ timeoutMs: this.timeoutMs,
199
+ retries: this.retries,
200
+ body: {
201
+ filename: options.filename,
202
+ fileSizeBytes: options.fileSizeBytes,
203
+ resolutions: options.resolutions,
204
+ isPublic: options.isPublic,
205
+ ...options.thumbnailTimestampsSeconds !== void 0 && {
206
+ thumbnail_timestamps_seconds: options.thumbnailTimestampsSeconds
207
+ },
208
+ ...options.customMetadata !== void 0 && {
209
+ custom_user_metadata: options.customMetadata
210
+ }
211
+ }
212
+ });
213
+ }
214
+ /**
215
+ * Notifies Hyperserve that the file has been uploaded to the presigned URL.
216
+ * Hyperserve verifies the object and queues transcoding.
217
+ * Call this after your frontend confirms the storage PUT is complete.
218
+ */
219
+ async completeUpload(videoId) {
220
+ return apiRequest({
221
+ method: "POST",
222
+ url: `${this.baseUrl}/video/${videoId}/complete-upload`,
223
+ apiKey: this.apiKey,
224
+ timeoutMs: this.timeoutMs,
225
+ retries: this.retries
226
+ });
227
+ }
228
+ /**
229
+ * Retrieves the current state of a video, including per-resolution status and playback URLs.
230
+ *
231
+ * @param videoId - The video ID returned by createVideo or uploadVideo.
232
+ * @param options.private - Return time-limited signed URLs instead of public URLs.
233
+ * @param options.expirationSeconds - Signed URL TTL when private is true. Defaults to 3600.
234
+ */
235
+ async getVideo(videoId, options) {
236
+ const isPrivate = options?.private === true;
237
+ const expiration = options?.expirationSeconds ?? 3600;
238
+ const url = isPrivate ? `${this.baseUrl}/video/${videoId}/private/${expiration}` : `${this.baseUrl}/video/${videoId}/public`;
239
+ return apiRequest({
240
+ method: "GET",
241
+ url,
242
+ apiKey: this.apiKey,
243
+ timeoutMs: this.timeoutMs,
244
+ retries: this.retries
245
+ });
246
+ }
247
+ /**
248
+ * Deletes a video and all associated resolutions and thumbnails.
249
+ */
250
+ async deleteVideo(videoId) {
251
+ return apiRequest({
252
+ method: "DELETE",
253
+ url: `${this.baseUrl}/video/${videoId}`,
254
+ apiKey: this.apiKey,
255
+ timeoutMs: this.timeoutMs,
256
+ retries: this.retries
257
+ });
258
+ }
259
+ /**
260
+ * Deletes a single resolution for a video.
261
+ */
262
+ async deleteResolution(resolutionId) {
263
+ return apiRequest({
264
+ method: "DELETE",
265
+ url: `${this.baseUrl}/video/resolution/${resolutionId}`,
266
+ apiKey: this.apiKey,
267
+ timeoutMs: this.timeoutMs,
268
+ retries: this.retries
269
+ });
270
+ }
271
+ /**
272
+ * Convenience method for server-side / script use cases.
273
+ * Wraps createVideo, the storage PUT, and completeUpload into a single call.
274
+ *
275
+ * Not suitable for the browser proxy pattern — use createVideo + putVideoToStorage
276
+ * from '@hyperserve/hyperserve-js/browser' + completeUpload separately for that flow.
277
+ */
278
+ async uploadVideo(options) {
279
+ const { file, filename, resolutions, isPublic, thumbnailTimestampsSeconds, customMetadata } = options;
280
+ const normalized = normalizeFile(file, filename, options.fileSizeBytes);
281
+ const upload = await this.createVideo({
282
+ filename,
283
+ fileSizeBytes: normalized.size,
284
+ resolutions,
285
+ isPublic,
286
+ ...thumbnailTimestampsSeconds !== void 0 && { thumbnailTimestampsSeconds },
287
+ ...customMetadata !== void 0 && { customMetadata }
288
+ });
289
+ await putToStorage(upload.uploadUrl, upload.contentType, normalized.body);
290
+ return this.completeUpload(upload.id);
291
+ }
292
+ };
293
+
294
+ // src/webhook.ts
295
+ var DEFAULT_TOLERANCE_MS = 3e5;
296
+ async function verifyWebhookSignature(options) {
297
+ const { signature, secret, body, toleranceMs = DEFAULT_TOLERANCE_MS } = options;
298
+ const dotIndex = signature.indexOf(".");
299
+ if (dotIndex === -1) return false;
300
+ const timestampStr = signature.slice(0, dotIndex);
301
+ const receivedHex = signature.slice(dotIndex + 1);
302
+ const timestamp = Number(timestampStr);
303
+ if (!Number.isInteger(timestamp) || timestamp < 0) return false;
304
+ if (Math.abs(Date.now() - timestamp) > toleranceMs) return false;
305
+ const receivedBytes = hexToBytes(receivedHex);
306
+ if (receivedBytes === null) return false;
307
+ const encoder = new TextEncoder();
308
+ const key = await crypto.subtle.importKey(
309
+ "raw",
310
+ encoder.encode(secret),
311
+ { name: "HMAC", hash: "SHA-256" },
312
+ false,
313
+ ["verify"]
314
+ );
315
+ return crypto.subtle.verify(
316
+ "HMAC",
317
+ key,
318
+ receivedBytes,
319
+ encoder.encode(`${timestampStr}.${body}`)
320
+ );
321
+ }
322
+ function hexToBytes(hex) {
323
+ if (hex.length === 0 || hex.length % 2 !== 0) return null;
324
+ const bytes = new Uint8Array(new ArrayBuffer(hex.length / 2));
325
+ for (let i = 0; i < hex.length; i += 2) {
326
+ const value = parseInt(hex.slice(i, i + 2), 16);
327
+ if (Number.isNaN(value)) return null;
328
+ bytes[i / 2] = value;
329
+ }
330
+ return bytes;
331
+ }
332
+
333
+ exports.HyperserveApiError = HyperserveApiError;
334
+ exports.HyperserveClient = HyperserveClient;
335
+ exports.HyperserveError = HyperserveError;
336
+ exports.HyperserveNotFoundError = HyperserveNotFoundError;
337
+ exports.HyperserveTimeoutError = HyperserveTimeoutError;
338
+ exports.HyperserveUploadError = HyperserveUploadError;
339
+ exports.HyperserveValidationError = HyperserveValidationError;
340
+ exports.verifyWebhookSignature = verifyWebhookSignature;
341
+ //# sourceMappingURL=index.cjs.map
342
+ //# sourceMappingURL=index.cjs.map