stophy 0.2.1 → 1.0.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/dist/index.js CHANGED
@@ -1,341 +1,621 @@
1
- import { createClient, createConfig } from '@hey-api/client-fetch';
2
-
3
- // src/generated/client.gen.ts
4
- var client = createClient(
5
- createConfig({
6
- baseUrl: "https://api.stophy.dev"
7
- })
8
- );
9
-
10
- // src/generated/sdk.gen.ts
11
- var getVideo = (options) => {
12
- return (options.client ?? client).post({
13
- security: [
14
- {
15
- scheme: "bearer",
16
- type: "http"
17
- }
18
- ],
19
- url: "/v1/video",
20
- ...options,
21
- headers: {
22
- "Content-Type": "application/json",
23
- ...options?.headers
24
- }
25
- });
26
- };
27
- var searchVideos = (options) => {
28
- return (options.client ?? client).post({
29
- security: [
30
- {
31
- scheme: "bearer",
32
- type: "http"
33
- }
34
- ],
35
- url: "/v1/search",
36
- ...options,
37
- headers: {
38
- "Content-Type": "application/json",
39
- ...options?.headers
40
- }
41
- });
42
- };
43
- var getChannel = (options) => {
44
- return (options.client ?? client).post({
45
- security: [
46
- {
47
- scheme: "bearer",
48
- type: "http"
49
- }
50
- ],
51
- url: "/v1/channel",
52
- ...options,
53
- headers: {
54
- "Content-Type": "application/json",
55
- ...options?.headers
56
- }
57
- });
58
- };
59
- var getPlaylist = (options) => {
60
- return (options.client ?? client).post({
61
- security: [
62
- {
63
- scheme: "bearer",
64
- type: "http"
65
- }
66
- ],
67
- url: "/v1/playlist",
68
- ...options,
69
- headers: {
70
- "Content-Type": "application/json",
71
- ...options?.headers
72
- }
73
- });
74
- };
75
- var getSuggestions = (options) => {
76
- return (options.client ?? client).get({
77
- security: [
78
- {
79
- scheme: "bearer",
80
- type: "http"
81
- }
82
- ],
83
- url: "/v1/suggest",
84
- ...options
85
- });
86
- };
87
- var getCredits = (options) => {
88
- return (options?.client ?? client).get({
89
- security: [
90
- {
91
- scheme: "bearer",
92
- type: "http"
93
- }
94
- ],
95
- url: "/v1/credits",
96
- ...options
97
- });
98
- };
99
- var getLogs = (options) => {
100
- return (options?.client ?? client).get({
101
- security: [
102
- {
103
- scheme: "bearer",
104
- type: "http"
105
- }
106
- ],
107
- url: "/v1/logs",
108
- ...options
109
- });
110
- };
111
- var getUsage = (options) => {
112
- return (options?.client ?? client).get({
113
- security: [
114
- {
115
- scheme: "bearer",
116
- type: "http"
117
- }
118
- ],
119
- url: "/v1/usage",
120
- ...options
121
- });
122
- };
123
-
124
1
  // src/errors.ts
125
2
  var StophyError = class extends Error {
126
3
  code;
4
+ retryable;
5
+ retryAfterSeconds;
127
6
  status;
128
7
  requestId;
129
- details;
130
8
  constructor(message, options) {
131
9
  super(message);
132
10
  this.name = "StophyError";
133
11
  this.status = options.status;
134
12
  this.code = options.code;
13
+ this.retryable = options.retryable;
14
+ this.retryAfterSeconds = options.retryAfterSeconds;
135
15
  this.requestId = options.requestId;
136
- this.details = options.details;
137
16
  }
138
17
  };
139
18
 
140
- // src/transport.ts
141
- var DEFAULT_BASE_URL = "https://api.stophy.dev";
142
- var RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
143
- function createTransport(options) {
144
- const baseFetch = options.fetch ?? globalThis.fetch;
145
- return createClient(
146
- createConfig({
147
- baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
148
- auth: () => options.apiKey,
149
- headers: options.headers,
150
- fetch: withRetries(
151
- baseFetch,
152
- options.maxRetries ?? 2,
153
- options.retryInitialDelayMs ?? 500
154
- )
155
- })
19
+ // src/account.ts
20
+ function usage(call) {
21
+ return call({ method: "GET", path: "/v1/usage" }).then(parseUsage);
22
+ }
23
+ function logs(call, query) {
24
+ const params = {};
25
+ if (query?.apiKeyId !== void 0) params.apiKeyId = query.apiKeyId;
26
+ if (query?.days !== void 0) params.days = query.days;
27
+ if (query?.endpoint !== void 0) params.endpoint = query.endpoint;
28
+ if (query?.page !== void 0) params.page = query.page;
29
+ return call({ method: "GET", path: "/v1/logs", query: params }).then(
30
+ parseLogs
156
31
  );
157
32
  }
158
- function withRetries(baseFetch, maxRetries, initialDelayMs) {
159
- if (maxRetries <= 0) return baseFetch;
160
- return async (input, init) => {
161
- for (let attempt = 0; ; attempt++) {
162
- const request = input instanceof Request ? input.clone() : input;
163
- try {
164
- const response = await baseFetch(request, init);
165
- if (attempt < maxRetries && RETRYABLE_STATUS.has(response.status)) {
166
- await sleep(
167
- retryAfterMs(response) ?? backoffMs(attempt, initialDelayMs)
168
- );
169
- continue;
170
- }
171
- return response;
172
- } catch (error) {
173
- if (attempt >= maxRetries) throw error;
174
- await sleep(backoffMs(attempt, initialDelayMs));
175
- }
176
- }
33
+ function parseUsage(value) {
34
+ if (!isRecord(value)) return unexpected("usage");
35
+ const balanceMicros = numberField(value, "balanceMicros");
36
+ const creditsUsed = numberField(value, "creditsUsed");
37
+ const requestCount = numberField(value, "requestCount");
38
+ return { balanceMicros, creditsUsed, requestCount };
39
+ }
40
+ function parseLogs(value) {
41
+ if (!isRecord(value) || !Array.isArray(value.logs) || !Array.isArray(value.endpoints)) {
42
+ return unexpected("logs");
43
+ }
44
+ return {
45
+ endpoints: value.endpoints.map((item) => {
46
+ if (typeof item !== "string") return unexpected("logs.endpoints");
47
+ return item;
48
+ }),
49
+ logs: value.logs.map(parseLog),
50
+ page: numberField(value, "page"),
51
+ pageSize: numberField(value, "pageSize"),
52
+ total: numberField(value, "total"),
53
+ totalPages: numberField(value, "totalPages")
177
54
  };
178
55
  }
179
- function backoffMs(attempt, base) {
180
- const window = base * 2 ** attempt;
181
- return window / 2 + Math.random() * (window / 2);
56
+ function parseLog(value) {
57
+ if (!isRecord(value)) return unexpected("logs.logs");
58
+ return {
59
+ apiKeyId: stringOrNull(value, "apiKeyId"),
60
+ apiKeyName: stringOrNull(value, "apiKeyName"),
61
+ createdAt: stringField(value, "createdAt"),
62
+ credits: numberField(value, "credits"),
63
+ durationMs: numberOrNull(value, "durationMs"),
64
+ endpoint: stringField(value, "endpoint"),
65
+ id: stringField(value, "id"),
66
+ method: stringField(value, "method"),
67
+ response: stringField(value, "response"),
68
+ status: numberField(value, "status")
69
+ };
182
70
  }
183
- function retryAfterMs(response) {
184
- const header = response.headers.get("retry-after");
185
- if (!header) return null;
186
- const seconds = Number(header);
187
- if (!Number.isNaN(seconds)) return seconds * 1e3;
188
- const date = Date.parse(header);
189
- return Number.isNaN(date) ? null : Math.max(0, date - Date.now());
71
+ function numberField(record, field) {
72
+ const value = record[field];
73
+ if (typeof value !== "number") return unexpected(field);
74
+ return value;
190
75
  }
191
- function sleep(ms) {
192
- return new Promise((resolve) => setTimeout(resolve, ms));
76
+ function numberOrNull(record, field) {
77
+ const value = record[field];
78
+ if (value === null) return null;
79
+ if (typeof value !== "number") return unexpected(field);
80
+ return value;
193
81
  }
194
- function unwrap(result) {
195
- if (result.error === void 0 && result.data !== void 0)
196
- return result.data;
197
- const error = result.error ?? {};
198
- throw new StophyError(
199
- error.error ?? `Stophy request failed with status ${result.response.status}`,
200
- {
201
- status: result.response.status,
202
- code: error.code,
203
- requestId: result.response.headers.get("x-request-id") ?? void 0,
204
- details: error.details
205
- }
206
- );
82
+ function stringField(record, field) {
83
+ const value = record[field];
84
+ if (typeof value !== "string") return unexpected(field);
85
+ return value;
207
86
  }
208
-
209
- // src/methods/account.ts
210
- function credits(client2) {
211
- return getCredits({ client: client2 }).then(unwrap);
87
+ function stringOrNull(record, field) {
88
+ const value = record[field];
89
+ if (value === null) return null;
90
+ if (typeof value !== "string") return unexpected(field);
91
+ return value;
212
92
  }
213
- function logs(client2, query) {
214
- return getLogs({ client: client2, query }).then(unwrap);
93
+ function unexpected(field) {
94
+ throw new StophyError(`Stophy returned an unexpected ${field}`, {
95
+ status: 200,
96
+ retryable: false
97
+ });
215
98
  }
216
- function usage(client2, query) {
217
- return getUsage({ client: client2, query }).then(unwrap);
99
+ function isRecord(value) {
100
+ return typeof value === "object" && value !== null && !Array.isArray(value);
218
101
  }
219
102
 
220
- // src/methods/channel.ts
221
- function channel(client2, urlOrBody, options = {}) {
222
- const body = typeof urlOrBody === "string" ? { channelUrl: urlOrBody, ...options } : urlOrBody;
223
- return getChannel({ client: client2, body }).then(unwrap);
103
+ // src/bind.ts
104
+ function post(path, call) {
105
+ function run(body, options) {
106
+ return call({ method: "POST", path, body, options });
107
+ }
108
+ return run;
224
109
  }
225
-
226
- // src/methods/playlist.ts
227
- function playlist(client2, urlOrBody, options = {}) {
228
- const body = typeof urlOrBody === "string" ? { playlistUrl: urlOrBody, ...options } : urlOrBody;
229
- return getPlaylist({ client: client2, body }).then(unwrap);
110
+ function postOptional(path, call) {
111
+ function run(body, options) {
112
+ if (isMarkdownOptions(body) && options === void 0) {
113
+ return call({ method: "POST", path, body: {}, options: body });
114
+ }
115
+ return call({ method: "POST", path, body: body ?? {}, options });
116
+ }
117
+ return run;
230
118
  }
231
-
232
- // src/methods/search.ts
233
- function search(client2, queryOrBody, options = {}) {
234
- if (typeof queryOrBody === "string" && !queryOrBody.trim()) {
235
- throw new Error("query cannot be empty");
119
+ function get(path, call) {
120
+ function run(options) {
121
+ return call({ method: "GET", path, options });
236
122
  }
237
- const body = typeof queryOrBody === "string" ? { q: queryOrBody, ...options } : queryOrBody;
238
- return searchVideos({ client: client2, body }).then(unwrap);
123
+ return run;
124
+ }
125
+ function isMarkdownOptions(value) {
126
+ if (typeof value !== "object" || value === null) return false;
127
+ return "format" in value && value.format === "markdown";
239
128
  }
240
129
 
241
- // src/methods/suggest.ts
242
- function suggest(client2, queryOrOptions, options = {}) {
243
- if (typeof queryOrOptions === "string" && !queryOrOptions.trim()) {
244
- throw new Error("query cannot be empty");
245
- }
246
- const query = typeof queryOrOptions === "string" ? { q: queryOrOptions, ...options } : queryOrOptions;
247
- return getSuggestions({ client: client2, query }).then(unwrap);
130
+ // src/generated/surface.gen.ts
131
+ function bindSurface(call) {
132
+ return {
133
+ endpoints: get("/v1/endpoints", call),
134
+ web: {
135
+ search: post("/v1/web/search", call),
136
+ news: postOptional("/v1/web/news", call),
137
+ contacts: post("/v1/web/contacts", call)
138
+ },
139
+ youtube: {
140
+ search: post("/v1/youtube/search", call),
141
+ video: post("/v1/youtube/video", call),
142
+ transcript: post("/v1/youtube/transcript", call),
143
+ comments: Object.assign(post("/v1/youtube/comments", call), {
144
+ replies: post("/v1/youtube/comments/replies", call)
145
+ }),
146
+ channel: post("/v1/youtube/channel", call),
147
+ playlist: post("/v1/youtube/playlist", call),
148
+ suggest: post("/v1/youtube/suggest", call)
149
+ },
150
+ reddit: {
151
+ search: post("/v1/reddit/search", call),
152
+ post: post("/v1/reddit/post", call),
153
+ comments: {
154
+ more: post("/v1/reddit/comments/more", call)
155
+ },
156
+ subreddit: post("/v1/reddit/subreddit", call),
157
+ user: post("/v1/reddit/user", call),
158
+ domain: post("/v1/reddit/domain", call)
159
+ },
160
+ maps: {
161
+ search: post("/v1/maps/search", call),
162
+ place: post("/v1/maps/place", call),
163
+ reviews: post("/v1/maps/reviews", call)
164
+ },
165
+ instagram: {
166
+ profile: post("/v1/instagram/profile", call),
167
+ posts: post("/v1/instagram/posts", call),
168
+ post: post("/v1/instagram/post", call),
169
+ url: post("/v1/instagram/url", call),
170
+ search: post("/v1/instagram/search", call),
171
+ transcript: post("/v1/instagram/transcript", call),
172
+ comments: Object.assign(post("/v1/instagram/comments", call), {
173
+ replies: post("/v1/instagram/comments/replies", call)
174
+ })
175
+ },
176
+ tiktok: {
177
+ profile: post("/v1/tiktok/profile", call),
178
+ video: post("/v1/tiktok/video", call),
179
+ url: post("/v1/tiktok/url", call),
180
+ transcript: post("/v1/tiktok/transcript", call),
181
+ posts: post("/v1/tiktok/posts", call),
182
+ hashtag: post("/v1/tiktok/hashtag", call),
183
+ comments: Object.assign(post("/v1/tiktok/comments", call), {
184
+ replies: post("/v1/tiktok/comments/replies", call)
185
+ }),
186
+ search: post("/v1/tiktok/search", call),
187
+ ads: {
188
+ search: postOptional("/v1/tiktok/ads/search", call),
189
+ ad: post("/v1/tiktok/ads/ad", call)
190
+ }
191
+ },
192
+ bluesky: {
193
+ profile: post("/v1/bluesky/profile", call),
194
+ posts: post("/v1/bluesky/posts", call),
195
+ post: post("/v1/bluesky/post", call),
196
+ followers: post("/v1/bluesky/followers", call)
197
+ },
198
+ mastodon: {
199
+ profile: post("/v1/mastodon/profile", call),
200
+ posts: post("/v1/mastodon/posts", call),
201
+ post: post("/v1/mastodon/post", call),
202
+ hashtag: post("/v1/mastodon/hashtag", call)
203
+ },
204
+ threads: {
205
+ profile: post("/v1/threads/profile", call),
206
+ posts: post("/v1/threads/posts", call),
207
+ post: post("/v1/threads/post", call),
208
+ search: post("/v1/threads/search", call)
209
+ },
210
+ telegram: {
211
+ channel: post("/v1/telegram/channel", call),
212
+ posts: post("/v1/telegram/posts", call),
213
+ post: post("/v1/telegram/post", call)
214
+ },
215
+ meta: {
216
+ ads: {
217
+ search: post("/v1/meta/ads/search", call),
218
+ page: post("/v1/meta/ads/page", call),
219
+ ad: post("/v1/meta/ads/ad", call)
220
+ }
221
+ },
222
+ linkedin: {
223
+ jobs: {
224
+ search: postOptional("/v1/linkedin/jobs/search", call),
225
+ job: post("/v1/linkedin/jobs/job", call)
226
+ },
227
+ ads: {
228
+ search: postOptional("/v1/linkedin/ads/search", call),
229
+ ad: post("/v1/linkedin/ads/ad", call)
230
+ },
231
+ company: Object.assign(post("/v1/linkedin/company", call), {
232
+ posts: post("/v1/linkedin/company/posts", call)
233
+ }),
234
+ profile: post("/v1/linkedin/profile", call),
235
+ posts: postOptional("/v1/linkedin/posts", call)
236
+ },
237
+ zillow: {
238
+ search: postOptional("/v1/zillow/search", call),
239
+ property: post("/v1/zillow/property", call)
240
+ },
241
+ google: {
242
+ ads: {
243
+ advertisers: post("/v1/google/ads/advertisers", call),
244
+ search: postOptional("/v1/google/ads/search", call),
245
+ ad: post("/v1/google/ads/ad", call)
246
+ },
247
+ suggest: post("/v1/google/suggest", call),
248
+ trends: {
249
+ interest: post("/v1/google/trends/interest", call),
250
+ regions: post("/v1/google/trends/regions", call),
251
+ related: post("/v1/google/trends/related", call),
252
+ trending: postOptional("/v1/google/trends/trending", call)
253
+ }
254
+ },
255
+ upwork: {
256
+ search: postOptional("/v1/upwork/search", call),
257
+ job: post("/v1/upwork/job", call)
258
+ },
259
+ amazon: {
260
+ suggest: post("/v1/amazon/suggest", call),
261
+ search: postOptional("/v1/amazon/search", call),
262
+ product: post("/v1/amazon/product", call),
263
+ bestsellers: post("/v1/amazon/bestsellers", call)
264
+ },
265
+ site: {
266
+ map: post("/v1/site/map", call),
267
+ seo: post("/v1/site/seo", call)
268
+ },
269
+ domain: {
270
+ whois: post("/v1/domain/whois", call),
271
+ dns: post("/v1/domain/dns", call),
272
+ tech: post("/v1/domain/tech", call)
273
+ },
274
+ email: {
275
+ check: post("/v1/email/check", call)
276
+ },
277
+ crypto: {
278
+ coins: postOptional("/v1/crypto/coins", call),
279
+ coin: post("/v1/crypto/coin", call),
280
+ history: post("/v1/crypto/history", call),
281
+ trending: postOptional("/v1/crypto/trending", call),
282
+ categories: postOptional("/v1/crypto/categories", call),
283
+ movers: postOptional("/v1/crypto/movers", call),
284
+ new: postOptional("/v1/crypto/new", call),
285
+ dex: {
286
+ search: post("/v1/crypto/dex/search", call),
287
+ pairs: post("/v1/crypto/dex/pairs", call),
288
+ token: post("/v1/crypto/dex/token", call),
289
+ new: postOptional("/v1/crypto/dex/new", call)
290
+ },
291
+ pump: {
292
+ coins: postOptional("/v1/crypto/pump/coins", call),
293
+ coin: post("/v1/crypto/pump/coin", call),
294
+ trades: post("/v1/crypto/pump/trades", call)
295
+ },
296
+ wallet: post("/v1/crypto/wallet", call),
297
+ token: {
298
+ holders: post("/v1/crypto/token/holders", call)
299
+ },
300
+ binance: {
301
+ announcements: postOptional("/v1/crypto/binance/announcements", call)
302
+ }
303
+ },
304
+ indeed: {
305
+ search: postOptional("/v1/indeed/search", call),
306
+ job: post("/v1/indeed/job", call)
307
+ },
308
+ tripadvisor: {
309
+ search: post("/v1/tripadvisor/search", call),
310
+ place: post("/v1/tripadvisor/place", call),
311
+ reviews: post("/v1/tripadvisor/reviews", call)
312
+ },
313
+ googletravel: {
314
+ flights: post("/v1/googletravel/flights", call)
315
+ },
316
+ shopify: {
317
+ products: post("/v1/shopify/products", call),
318
+ collections: post("/v1/shopify/collections", call),
319
+ store: post("/v1/shopify/store", call)
320
+ },
321
+ walmart: {
322
+ search: post("/v1/walmart/search", call),
323
+ product: post("/v1/walmart/product", call)
324
+ },
325
+ aliexpress: {
326
+ search: post("/v1/aliexpress/search", call),
327
+ product: post("/v1/aliexpress/product", call)
328
+ },
329
+ appstore: {
330
+ app: post("/v1/appstore/app", call),
331
+ search: post("/v1/appstore/search", call),
332
+ reviews: post("/v1/appstore/reviews", call),
333
+ top: postOptional("/v1/appstore/top", call)
334
+ },
335
+ googleplay: {
336
+ app: post("/v1/googleplay/app", call),
337
+ search: post("/v1/googleplay/search", call),
338
+ reviews: post("/v1/googleplay/reviews", call)
339
+ },
340
+ airbnb: {
341
+ search: post("/v1/airbnb/search", call),
342
+ listing: post("/v1/airbnb/listing", call),
343
+ calendar: post("/v1/airbnb/calendar", call),
344
+ reviews: post("/v1/airbnb/reviews", call)
345
+ },
346
+ redfin: {
347
+ search: post("/v1/redfin/search", call),
348
+ property: post("/v1/redfin/property", call)
349
+ },
350
+ realtor: {
351
+ search: post("/v1/realtor/search", call),
352
+ property: post("/v1/realtor/property", call)
353
+ },
354
+ rightmove: {
355
+ search: post("/v1/rightmove/search", call),
356
+ property: post("/v1/rightmove/property", call)
357
+ },
358
+ immoscout: {
359
+ search: post("/v1/immoscout/search", call),
360
+ listing: post("/v1/immoscout/listing", call)
361
+ },
362
+ pinterest: {
363
+ search: post("/v1/pinterest/search", call),
364
+ pin: post("/v1/pinterest/pin", call),
365
+ board: post("/v1/pinterest/board", call),
366
+ user: post("/v1/pinterest/user", call),
367
+ ads: {
368
+ search: post("/v1/pinterest/ads/search", call),
369
+ ad: post("/v1/pinterest/ads/ad", call)
370
+ }
371
+ },
372
+ x: {
373
+ tweet: post("/v1/x/tweet", call)
374
+ },
375
+ kick: {
376
+ channel: post("/v1/kick/channel", call),
377
+ videos: post("/v1/kick/videos", call),
378
+ clips: post("/v1/kick/clips", call)
379
+ },
380
+ finance: {
381
+ quote: post("/v1/finance/quote", call),
382
+ history: post("/v1/finance/history", call),
383
+ search: post("/v1/finance/search", call),
384
+ profile: post("/v1/finance/profile", call)
385
+ },
386
+ microsoft: {
387
+ ads: {
388
+ search: postOptional("/v1/microsoft/ads/search", call),
389
+ ad: post("/v1/microsoft/ads/ad", call),
390
+ advertisers: post("/v1/microsoft/ads/advertisers", call)
391
+ }
392
+ },
393
+ snapchat: {
394
+ profile: post("/v1/snapchat/profile", call),
395
+ ads: {
396
+ search: post("/v1/snapchat/ads/search", call),
397
+ ad: post("/v1/snapchat/ads/ad", call)
398
+ }
399
+ },
400
+ tumblr: {
401
+ blog: post("/v1/tumblr/blog", call),
402
+ posts: post("/v1/tumblr/posts", call),
403
+ post: post("/v1/tumblr/post", call),
404
+ search: post("/v1/tumblr/search", call)
405
+ },
406
+ quora: {
407
+ question: post("/v1/quora/question", call)
408
+ }
409
+ };
248
410
  }
249
411
 
250
- // src/methods/video.ts
251
- async function video(client2, body) {
252
- return unwrap(await getVideo({ client: client2, body }));
412
+ // package.json
413
+ var version = "1.0.0";
414
+
415
+ // src/transport.ts
416
+ var DEFAULT_BASE_URL = "https://api.stophy.dev";
417
+ var DEFAULT_TIMEOUT_MS = 3e4;
418
+ var MAX_RETRY_WAIT_SECONDS = 60;
419
+ var RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
420
+ var USER_AGENT = `stophy-typescript/${version}`;
421
+ function createCaller(options) {
422
+ const baseFetch = options.fetch ?? globalThis.fetch;
423
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
424
+ const retry = {
425
+ maxRetries: options.maxRetries ?? 2,
426
+ initialDelayMs: options.retryInitialDelayMs ?? 500,
427
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS
428
+ };
429
+ return async (spec) => {
430
+ const markdown = spec.options?.format === "markdown";
431
+ const headers = new Headers(options.headers);
432
+ if (typeof document === "undefined" && !headers.has("user-agent")) {
433
+ headers.set("user-agent", USER_AGENT);
434
+ }
435
+ if (options.apiKey) {
436
+ headers.set("authorization", `Bearer ${options.apiKey}`);
437
+ }
438
+ headers.set("accept", markdown ? "text/markdown" : "application/json");
439
+ if (spec.body !== void 0)
440
+ headers.set("content-type", "application/json");
441
+ const url = withQuery(`${baseUrl}${spec.path}`, spec.query);
442
+ return send(
443
+ baseFetch,
444
+ url,
445
+ (signal) => ({
446
+ method: spec.method,
447
+ headers,
448
+ body: spec.body === void 0 ? void 0 : JSON.stringify(spec.body),
449
+ signal
450
+ }),
451
+ markdown,
452
+ spec.options?.signal,
453
+ retry
454
+ );
455
+ };
253
456
  }
254
- function videoDetails(client2, videoUrl) {
255
- return video(client2, { type: "details", videoUrl });
457
+ async function send(baseFetch, url, init, markdown, signal, retry) {
458
+ for (let n = 0; ; n += 1) {
459
+ const deadline = withDeadline(signal, retry.timeoutMs);
460
+ let waitMs;
461
+ try {
462
+ const response = await baseFetch(url, init(deadline.signal)).catch(
463
+ (error) => {
464
+ if (deadline.signal.aborted || n >= retry.maxRetries) throw error;
465
+ return void 0;
466
+ }
467
+ );
468
+ if (response === void 0) {
469
+ waitMs = backoffMs(n, retry.initialDelayMs);
470
+ } else if (n < retry.maxRetries && RETRYABLE_STATUS.has(response.status)) {
471
+ const error = await errorFrom(response);
472
+ const seconds = error.retryAfterSeconds;
473
+ if (seconds !== void 0 && seconds > MAX_RETRY_WAIT_SECONDS) {
474
+ throw error;
475
+ }
476
+ waitMs = seconds === void 0 ? backoffMs(n, retry.initialDelayMs) : seconds * 1e3;
477
+ } else {
478
+ return await readBody(response, markdown);
479
+ }
480
+ } finally {
481
+ deadline.clear();
482
+ }
483
+ await sleep(waitMs);
484
+ }
256
485
  }
257
- function transcript(client2, videoUrl) {
258
- return video(client2, { type: "transcript", videoUrl });
486
+ function withDeadline(signal, timeoutMs) {
487
+ const controller = new AbortController();
488
+ const abort = () => controller.abort(signal?.reason);
489
+ if (signal?.aborted) abort();
490
+ else signal?.addEventListener("abort", abort, { once: true });
491
+ const timer = setTimeout(
492
+ () => controller.abort(
493
+ new DOMException(
494
+ `Stophy request timed out after ${timeoutMs} ms`,
495
+ "TimeoutError"
496
+ )
497
+ ),
498
+ timeoutMs
499
+ );
500
+ return {
501
+ signal: controller.signal,
502
+ clear: () => {
503
+ clearTimeout(timer);
504
+ signal?.removeEventListener("abort", abort);
505
+ }
506
+ };
259
507
  }
260
- function comments(client2, videoUrl, options = {}) {
261
- return video(client2, {
262
- type: "comments",
263
- videoUrl,
264
- ...options
265
- });
508
+ async function readBody(response, markdown) {
509
+ if (!response.ok) throw await errorFrom(response);
510
+ const text = await response.text();
511
+ if (markdown) return text;
512
+ if (!text) {
513
+ throw failed(response, "Stophy returned an empty response", {});
514
+ }
515
+ try {
516
+ return JSON.parse(text);
517
+ } catch {
518
+ throw failed(response, "Stophy returned a non-JSON response", {});
519
+ }
266
520
  }
267
- function replies(client2, continuationToken) {
268
- return video(client2, {
269
- type: "replies",
270
- continuationToken
271
- });
521
+ async function errorFrom(response) {
522
+ const text = await response.text();
523
+ let body;
524
+ if (text) {
525
+ try {
526
+ body = JSON.parse(text);
527
+ } catch {
528
+ body = void 0;
529
+ }
530
+ }
531
+ const error = readError(body);
532
+ const retryAfterSeconds = error.retryAfterSeconds ?? headerRetryAfter(response);
533
+ return failed(
534
+ response,
535
+ error.message ?? `Stophy request failed with status ${response.status}`,
536
+ {
537
+ code: error.code,
538
+ retryable: error.retryable ?? RETRYABLE_STATUS.has(response.status),
539
+ retryAfterSeconds,
540
+ requestId: error.requestId ?? headerRequestId(response)
541
+ }
542
+ );
272
543
  }
273
- function liveChat(client2, videoUrl, options = {}) {
274
- return video(client2, {
275
- type: "livechat",
276
- videoUrl,
277
- ...options
544
+ function failed(response, message, options) {
545
+ return new StophyError(message, {
546
+ status: response.status,
547
+ code: options.code,
548
+ retryable: options.retryable ?? false,
549
+ retryAfterSeconds: options.retryAfterSeconds,
550
+ requestId: options.requestId ?? headerRequestId(response)
278
551
  });
279
552
  }
553
+ function readError(body) {
554
+ if (!isRecord2(body) || !isRecord2(body.error)) return {};
555
+ const error = body.error;
556
+ return {
557
+ code: typeof error.code === "string" ? error.code : void 0,
558
+ message: typeof error.message === "string" ? error.message : void 0,
559
+ retryable: typeof error.retryable === "boolean" ? error.retryable : void 0,
560
+ retryAfterSeconds: typeof error.retryAfterSeconds === "number" ? error.retryAfterSeconds : void 0,
561
+ requestId: typeof error.requestId === "string" ? error.requestId : void 0
562
+ };
563
+ }
564
+ function isRecord2(value) {
565
+ return typeof value === "object" && value !== null && !Array.isArray(value);
566
+ }
567
+ function headerRequestId(response) {
568
+ return response.headers.get("x-request-id") ?? void 0;
569
+ }
570
+ function headerRetryAfter(response) {
571
+ const header = response.headers.get("retry-after");
572
+ if (!header) return void 0;
573
+ const seconds = Number(header);
574
+ if (Number.isFinite(seconds)) return seconds;
575
+ const date = Date.parse(header);
576
+ if (Number.isNaN(date)) return void 0;
577
+ return Math.max(0, Math.ceil((date - Date.now()) / 1e3));
578
+ }
579
+ function withQuery(url, query) {
580
+ if (!query) return url;
581
+ const params = new URLSearchParams();
582
+ for (const [key, value] of Object.entries(query)) {
583
+ if (value !== void 0) params.set(key, String(value));
584
+ }
585
+ const text = params.toString();
586
+ return text ? `${url}?${text}` : url;
587
+ }
588
+ function backoffMs(attempt, base) {
589
+ const window = base * 2 ** attempt;
590
+ return window / 2 + Math.random() * (window / 2);
591
+ }
592
+ function sleep(ms) {
593
+ return new Promise((resolve) => setTimeout(resolve, ms));
594
+ }
280
595
 
281
596
  // src/client.ts
282
597
  function env(name) {
283
598
  return typeof process === "undefined" ? void 0 : process.env[name];
284
599
  }
285
600
  var Stophy = class {
286
- client;
601
+ #call;
287
602
  constructor(input = {}) {
288
603
  const options = typeof input === "string" ? { apiKey: input } : input;
289
604
  const apiKey = options.apiKey?.trim() || env("STOPHY_API_KEY")?.trim();
290
- if (!apiKey) {
291
- throw new Error(
292
- "Stophy: provide `apiKey` or set the STOPHY_API_KEY environment variable."
293
- );
294
- }
295
- this.client = createTransport({
605
+ this.#call = createCaller({
296
606
  ...options,
297
607
  apiKey,
298
608
  baseUrl: options.baseUrl ?? env("STOPHY_BASE_URL")
299
609
  });
610
+ Object.assign(this, bindSurface(this.#call));
300
611
  }
301
- async video(body) {
302
- return video(this.client, body);
303
- }
304
- videoDetails(videoUrl) {
305
- return videoDetails(this.client, videoUrl);
306
- }
307
- transcript(videoUrl) {
308
- return transcript(this.client, videoUrl);
309
- }
310
- comments(videoUrl, options) {
311
- return comments(this.client, videoUrl, options);
312
- }
313
- replies(continuationToken) {
314
- return replies(this.client, continuationToken);
315
- }
316
- liveChat(videoUrl, options) {
317
- return liveChat(this.client, videoUrl, options);
318
- }
319
- async search(queryOrBody, options) {
320
- return search(this.client, queryOrBody, options);
321
- }
322
- async channel(urlOrBody, options) {
323
- return channel(this.client, urlOrBody, options);
324
- }
325
- async playlist(urlOrBody, options) {
326
- return playlist(this.client, urlOrBody, options);
327
- }
328
- async suggest(queryOrOptions, options) {
329
- return suggest(this.client, queryOrOptions, options);
330
- }
331
- credits() {
332
- return credits(this.client);
612
+ /** Current balance and lifetime usage. Requires an API key. */
613
+ usage() {
614
+ return usage(this.#call);
333
615
  }
616
+ /** Recent metered requests. Requires an API key. */
334
617
  logs(query) {
335
- return logs(this.client, query);
336
- }
337
- usage(query) {
338
- return usage(this.client, query);
618
+ return logs(this.#call, query);
339
619
  }
340
620
  };
341
621