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