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