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.js CHANGED
@@ -1,331 +1,596 @@
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 searchVideos = (options) => {
12
- return (options.client ?? client).post({
13
- security: [
14
- {
15
- scheme: "bearer",
16
- type: "http"
17
- }
18
- ],
19
- url: "/v1/search",
20
- ...options,
21
- headers: {
22
- "Content-Type": "application/json",
23
- ...options?.headers
24
- }
25
- });
26
- };
27
- var getVideo = (options) => {
28
- return (options.client ?? client).post({
29
- security: [
30
- {
31
- scheme: "bearer",
32
- type: "http"
33
- }
34
- ],
35
- url: "/v1/video",
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).post({
77
- security: [
78
- {
79
- scheme: "bearer",
80
- type: "http"
81
- }
82
- ],
83
- url: "/v1/suggest",
84
- ...options,
85
- headers: {
86
- "Content-Type": "application/json",
87
- ...options?.headers
88
- }
89
- });
90
- };
91
- var youtubeMusic = (options) => {
92
- return (options.client ?? client).post({
93
- security: [
94
- {
95
- scheme: "bearer",
96
- type: "http"
97
- }
98
- ],
99
- url: "/v1/music",
100
- ...options,
101
- headers: {
102
- "Content-Type": "application/json",
103
- ...options?.headers
104
- }
105
- });
106
- };
107
- var youtubeKids = (options) => {
108
- return (options.client ?? client).post({
109
- security: [
110
- {
111
- scheme: "bearer",
112
- type: "http"
113
- }
114
- ],
115
- url: "/v1/kids",
116
- ...options,
117
- headers: {
118
- "Content-Type": "application/json",
119
- ...options?.headers
120
- }
121
- });
122
- };
123
- var getCredits = (options) => {
124
- return (options?.client ?? client).get({
125
- security: [
126
- {
127
- scheme: "bearer",
128
- type: "http"
129
- }
130
- ],
131
- url: "/v1/credits",
132
- ...options
133
- });
134
- };
135
- var getUsage = (options) => {
136
- return (options?.client ?? client).get({
137
- security: [
138
- {
139
- scheme: "bearer",
140
- type: "http"
141
- }
142
- ],
143
- url: "/v1/usage",
144
- ...options
145
- });
146
- };
147
- var getLogs = (options) => {
148
- return (options?.client ?? client).get({
149
- security: [
150
- {
151
- scheme: "bearer",
152
- type: "http"
153
- }
154
- ],
155
- url: "/v1/logs",
156
- ...options
157
- });
158
- };
159
-
160
1
  // src/errors.ts
161
2
  var StophyError = class extends Error {
162
3
  code;
4
+ retryable;
5
+ retryAfterSeconds;
163
6
  status;
164
7
  requestId;
165
- details;
166
8
  constructor(message, options) {
167
9
  super(message);
168
10
  this.name = "StophyError";
169
11
  this.status = options.status;
170
12
  this.code = options.code;
13
+ this.retryable = options.retryable;
14
+ this.retryAfterSeconds = options.retryAfterSeconds;
171
15
  this.requestId = options.requestId;
172
- this.details = options.details;
173
16
  }
174
17
  };
175
18
 
176
- // src/transport.ts
177
- var DEFAULT_BASE_URL = "https://api.stophy.dev";
178
- var RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
179
- function createTransport(options) {
180
- const baseFetch = options.fetch ?? globalThis.fetch;
181
- return createClient(
182
- createConfig({
183
- baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
184
- auth: () => options.apiKey,
185
- headers: options.headers,
186
- fetch: withRetries(
187
- baseFetch,
188
- options.maxRetries ?? 2,
189
- options.retryInitialDelayMs ?? 500
190
- )
191
- })
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
192
31
  );
193
32
  }
194
- function withRetries(baseFetch, maxRetries, initialDelayMs) {
195
- if (maxRetries <= 0) return baseFetch;
196
- return async (input, init) => {
197
- for (let attempt = 0; ; attempt++) {
198
- const request = input instanceof Request ? input.clone() : input;
199
- try {
200
- const response = await baseFetch(request, init);
201
- if (attempt < maxRetries && RETRYABLE_STATUS.has(response.status)) {
202
- await sleep(
203
- retryAfterMs(response) ?? backoffMs(attempt, initialDelayMs)
204
- );
205
- continue;
206
- }
207
- return response;
208
- } catch (error) {
209
- if (attempt >= maxRetries) throw error;
210
- await sleep(backoffMs(attempt, initialDelayMs));
211
- }
212
- }
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")
213
54
  };
214
55
  }
215
- function backoffMs(attempt, base) {
216
- const window = base * 2 ** attempt;
217
- 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
+ };
218
70
  }
219
- function retryAfterMs(response) {
220
- const header = response.headers.get("retry-after");
221
- if (!header) return null;
222
- const seconds = Number(header);
223
- if (!Number.isNaN(seconds)) return seconds * 1e3;
224
- const date = Date.parse(header);
225
- 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;
226
75
  }
227
- function sleep(ms) {
228
- 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;
229
81
  }
230
- function unwrap(result) {
231
- if (result.error === void 0 && result.data !== void 0)
232
- return result.data;
233
- const error = result.error ?? {};
234
- throw new StophyError(
235
- error.error ?? `Stophy request failed with status ${result.response.status}`,
236
- {
237
- status: result.response.status,
238
- code: error.code,
239
- requestId: result.response.headers.get("x-request-id") ?? void 0,
240
- details: error.details
241
- }
242
- );
82
+ function stringField(record, field) {
83
+ const value = record[field];
84
+ if (typeof value !== "string") return unexpected(field);
85
+ return value;
243
86
  }
244
-
245
- // src/methods/account.ts
246
- function credits(client2) {
247
- 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;
248
92
  }
249
- function logs(client2, query) {
250
- 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
+ });
251
98
  }
252
- function usage(client2, query) {
253
- return getUsage({ client: client2, query }).then(unwrap);
99
+ function isRecord(value) {
100
+ return typeof value === "object" && value !== null && !Array.isArray(value);
254
101
  }
255
102
 
256
- // src/methods/channel.ts
257
- function channel(client2, urlOrBody, options = {}) {
258
- const body = typeof urlOrBody === "string" ? { channelUrl: urlOrBody, ...options } : urlOrBody;
259
- 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;
260
109
  }
261
-
262
- // src/methods/kids.ts
263
- function kids(client2, body) {
264
- return youtubeKids({ 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;
118
+ }
119
+ function get(path, call) {
120
+ function run(options) {
121
+ return call({ method: "GET", path, options });
122
+ }
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";
265
128
  }
266
129
 
267
- // src/methods/music.ts
268
- function music(client2, body) {
269
- return youtubeMusic({ client: client2, body }).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
+ };
270
410
  }
271
411
 
272
- // src/methods/playlist.ts
273
- function playlist(client2, urlOrBody, options = {}) {
274
- const body = typeof urlOrBody === "string" ? { playlistUrl: urlOrBody, ...options } : urlOrBody;
275
- return getPlaylist({ client: client2, body }).then(
276
- unwrap
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
+ };
456
+ }
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
+ }
485
+ }
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
277
499
  );
500
+ return {
501
+ signal: controller.signal,
502
+ clear: () => {
503
+ clearTimeout(timer);
504
+ signal?.removeEventListener("abort", abort);
505
+ }
506
+ };
278
507
  }
279
-
280
- // src/methods/search.ts
281
- function search(client2, queryOrBody, options = {}) {
282
- if (typeof queryOrBody === "string" && !queryOrBody.trim()) {
283
- throw new Error("query cannot be empty");
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", {});
284
519
  }
285
- const body = typeof queryOrBody === "string" ? { q: queryOrBody, ...options } : queryOrBody;
286
- return searchVideos({ client: client2, body }).then(unwrap);
287
520
  }
288
-
289
- // src/methods/suggest.ts
290
- function suggest(client2, queryOrOptions, options = {}) {
291
- if (typeof queryOrOptions === "string" && !queryOrOptions.trim()) {
292
- throw new Error("query cannot be empty");
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
+ }
293
530
  }
294
- const body = typeof queryOrOptions === "string" ? { q: queryOrOptions, ...options } : queryOrOptions;
295
- return getSuggestions({ client: client2, body }).then(
296
- unwrap
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
+ }
297
542
  );
298
543
  }
299
-
300
- // src/methods/video.ts
301
- async function video(client2, body) {
302
- return unwrap(await getVideo({ client: client2, body }));
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)
551
+ });
303
552
  }
304
- function videoDetails(client2, videoUrl) {
305
- return video(client2, { type: "details", videoUrl });
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
+ };
306
563
  }
307
- function transcript(client2, videoUrl, options = {}) {
308
- return video(client2, { type: "transcript", videoUrl, ...options });
564
+ function isRecord2(value) {
565
+ return typeof value === "object" && value !== null && !Array.isArray(value);
309
566
  }
310
- function comments(client2, videoUrl, options = {}) {
311
- return video(client2, {
312
- type: "comments",
313
- videoUrl,
314
- ...options
315
- });
567
+ function headerRequestId(response) {
568
+ return response.headers.get("x-request-id") ?? void 0;
316
569
  }
317
- function replies(client2, continuationToken) {
318
- return video(client2, {
319
- type: "replies",
320
- continuationToken
321
- });
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));
322
578
  }
323
- function liveChat(client2, videoUrl, options = {}) {
324
- return video(client2, {
325
- type: "livechat",
326
- videoUrl,
327
- ...options
328
- });
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));
329
594
  }
330
595
 
331
596
  // src/client.ts
@@ -333,65 +598,24 @@ function env(name) {
333
598
  return typeof process === "undefined" ? void 0 : process.env[name];
334
599
  }
335
600
  var Stophy = class {
336
- client;
601
+ #call;
337
602
  constructor(input = {}) {
338
603
  const options = typeof input === "string" ? { apiKey: input } : input;
339
604
  const apiKey = options.apiKey?.trim() || env("STOPHY_API_KEY")?.trim();
340
- if (!apiKey) {
341
- throw new Error(
342
- "Stophy: provide `apiKey` or set the STOPHY_API_KEY environment variable."
343
- );
344
- }
345
- this.client = createTransport({
605
+ this.#call = createCaller({
346
606
  ...options,
347
607
  apiKey,
348
608
  baseUrl: options.baseUrl ?? env("STOPHY_BASE_URL")
349
609
  });
610
+ Object.assign(this, bindSurface(this.#call));
350
611
  }
351
- async video(body) {
352
- return video(this.client, body);
353
- }
354
- videoDetails(videoUrl) {
355
- return videoDetails(this.client, videoUrl);
356
- }
357
- transcript(videoUrl, options) {
358
- return transcript(this.client, videoUrl, options);
359
- }
360
- comments(videoUrl, options) {
361
- return comments(this.client, videoUrl, options);
362
- }
363
- replies(continuationToken) {
364
- return replies(this.client, continuationToken);
365
- }
366
- liveChat(videoUrl, options) {
367
- return liveChat(this.client, videoUrl, options);
368
- }
369
- async search(queryOrBody, options) {
370
- return search(this.client, queryOrBody, options);
371
- }
372
- async channel(urlOrBody, options) {
373
- return channel(this.client, urlOrBody, options);
374
- }
375
- async playlist(urlOrBody, options) {
376
- return playlist(this.client, urlOrBody, options);
377
- }
378
- async suggest(queryOrOptions, options) {
379
- return suggest(this.client, queryOrOptions, options);
380
- }
381
- music(body) {
382
- return music(this.client, body);
383
- }
384
- kids(body) {
385
- return kids(this.client, body);
386
- }
387
- credits() {
388
- return credits(this.client);
612
+ /** Current balance and lifetime usage. Requires an API key. */
613
+ usage() {
614
+ return usage(this.#call);
389
615
  }
616
+ /** Recent metered requests. Requires an API key. */
390
617
  logs(query) {
391
- return logs(this.client, query);
392
- }
393
- usage(query) {
394
- return usage(this.client, query);
618
+ return logs(this.#call, query);
395
619
  }
396
620
  };
397
621