instapaper-api 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,664 @@
1
+ // src/errors.ts
2
+ var InstapaperError = class extends Error {
3
+ constructor(message, options) {
4
+ super(message, options);
5
+ this.name = "InstapaperError";
6
+ Object.setPrototypeOf(this, new.target.prototype);
7
+ if (options && "cause" in options && this.cause === void 0) {
8
+ this.cause = options.cause;
9
+ }
10
+ }
11
+ };
12
+ var APIError = class extends InstapaperError {
13
+ /** HTTP status code. */
14
+ status;
15
+ /** The parsed JSON body, the raw text if it wasn't JSON, or `undefined` if empty. */
16
+ body;
17
+ constructor(message, status, body) {
18
+ super(message);
19
+ this.name = "APIError";
20
+ this.status = status;
21
+ this.body = body;
22
+ }
23
+ };
24
+ var BadRequestError = class extends APIError {
25
+ constructor(message, status = 400, body) {
26
+ super(message, status, body);
27
+ this.name = "BadRequestError";
28
+ }
29
+ };
30
+ var AuthenticationError = class extends APIError {
31
+ constructor(message, status = 401, body) {
32
+ super(message, status, body);
33
+ this.name = "AuthenticationError";
34
+ }
35
+ };
36
+ var QuotaExceededError = class extends APIError {
37
+ constructor(message, status = 402, body) {
38
+ super(message, status, body);
39
+ this.name = "QuotaExceededError";
40
+ }
41
+ };
42
+ var PermissionDeniedError = class extends APIError {
43
+ constructor(message, status = 403, body) {
44
+ super(message, status, body);
45
+ this.name = "PermissionDeniedError";
46
+ }
47
+ };
48
+ var NotFoundError = class extends APIError {
49
+ constructor(message, status = 404, body) {
50
+ super(message, status, body);
51
+ this.name = "NotFoundError";
52
+ }
53
+ };
54
+ var RateLimitError = class extends APIError {
55
+ constructor(message, status = 429, body) {
56
+ super(message, status, body);
57
+ this.name = "RateLimitError";
58
+ }
59
+ };
60
+ var ServerError = class extends APIError {
61
+ constructor(message, status = 500, body) {
62
+ super(message, status, body);
63
+ this.name = "ServerError";
64
+ }
65
+ };
66
+ var OAuthError = class extends InstapaperError {
67
+ status;
68
+ /** The OAuth error code, e.g. `invalid_grant`. */
69
+ error;
70
+ description;
71
+ constructor(status, error, description) {
72
+ super(`${error}: ${description}`);
73
+ this.name = "OAuthError";
74
+ this.status = status;
75
+ this.error = error;
76
+ this.description = description;
77
+ }
78
+ };
79
+ var InstapaperConnectionError = class extends InstapaperError {
80
+ constructor(message, options) {
81
+ super(message, options);
82
+ this.name = "InstapaperConnectionError";
83
+ }
84
+ };
85
+ var DEFAULT_MESSAGES = {
86
+ 400: "Bad request",
87
+ 401: "Authentication failed",
88
+ 402: "Quota exceeded",
89
+ 403: "Permission denied",
90
+ 404: "Not found",
91
+ 429: "Rate limit exceeded"
92
+ };
93
+ function apiErrorFor(status, body) {
94
+ const message = errorMessage(body) ?? defaultMessage(status);
95
+ switch (status) {
96
+ case 400:
97
+ return new BadRequestError(message, status, body);
98
+ case 401:
99
+ return new AuthenticationError(message, status, body);
100
+ case 402:
101
+ return new QuotaExceededError(message, status, body);
102
+ case 403:
103
+ return new PermissionDeniedError(message, status, body);
104
+ case 404:
105
+ return new NotFoundError(message, status, body);
106
+ case 429:
107
+ return new RateLimitError(message, status, body);
108
+ default:
109
+ if (status >= 500) return new ServerError(message, status, body);
110
+ return new APIError(message, status, body);
111
+ }
112
+ }
113
+ function defaultMessage(status) {
114
+ if (DEFAULT_MESSAGES[status]) return DEFAULT_MESSAGES[status];
115
+ if (status >= 500) return "Server error";
116
+ return `Request failed with status ${status}`;
117
+ }
118
+ function errorMessage(body) {
119
+ if (!body || typeof body !== "object") return void 0;
120
+ const error = body.error;
121
+ if (error && typeof error === "object") {
122
+ const message = error.message;
123
+ if (typeof message === "string" && message) return message;
124
+ }
125
+ return void 0;
126
+ }
127
+
128
+ // src/version.ts
129
+ var VERSION = "0.1.0";
130
+
131
+ // src/http.ts
132
+ var DEFAULT_BASE_URL = "https://www.instapaper.com";
133
+ var DEFAULT_TIMEOUT = 3e4;
134
+ var Transport = class {
135
+ baseUrl;
136
+ timeout;
137
+ fetchImpl;
138
+ constructor(options = {}) {
139
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
140
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
141
+ const globalFetch = globalThis.fetch;
142
+ const fetchImpl = options.fetch ?? (globalFetch ? globalFetch.bind(globalThis) : void 0);
143
+ if (!fetchImpl) {
144
+ throw new InstapaperError(
145
+ "No fetch implementation is available. Use Node 18 or newer, or pass options.fetch."
146
+ );
147
+ }
148
+ this.fetchImpl = fetchImpl;
149
+ }
150
+ url(path, query) {
151
+ let url = `${this.baseUrl}${path}`;
152
+ if (query) {
153
+ const params = new URLSearchParams();
154
+ for (const [key, value] of Object.entries(query)) {
155
+ if (value === void 0 || value === null) continue;
156
+ params.append(key, String(value));
157
+ }
158
+ const qs = params.toString();
159
+ if (qs) url += `?${qs}`;
160
+ }
161
+ return url;
162
+ }
163
+ async send(method, path, options = {}) {
164
+ const headers = { Accept: "application/json", ...options.headers };
165
+ if (canSetUserAgent()) headers["User-Agent"] = `instapaper-api-js/${VERSION}`;
166
+ const controller = this.timeout > 0 ? new AbortController() : void 0;
167
+ let timedOut = false;
168
+ const timer = controller ? setTimeout(() => {
169
+ timedOut = true;
170
+ controller.abort();
171
+ }, this.timeout) : void 0;
172
+ try {
173
+ const init = { method, headers, redirect: "manual" };
174
+ if (options.body !== void 0) init.body = options.body;
175
+ if (controller) init.signal = controller.signal;
176
+ const response = await this.fetchImpl(this.url(path, options.query), init);
177
+ const text = await response.text();
178
+ return { status: response.status, ok: response.ok, text };
179
+ } catch (err) {
180
+ if (timedOut) {
181
+ throw new InstapaperConnectionError(`Request timed out after ${this.timeout}ms`, {
182
+ cause: err
183
+ });
184
+ }
185
+ const detail = err instanceof Error ? err.message : String(err);
186
+ throw new InstapaperConnectionError(`Network error: ${detail}`, { cause: err });
187
+ } finally {
188
+ if (timer !== void 0) clearTimeout(timer);
189
+ }
190
+ }
191
+ };
192
+ function parseBody(text) {
193
+ if (!text.trim()) return void 0;
194
+ try {
195
+ return JSON.parse(text);
196
+ } catch {
197
+ return text;
198
+ }
199
+ }
200
+ var ApiClient = class {
201
+ constructor(transport, accessToken) {
202
+ this.transport = transport;
203
+ this.accessToken = accessToken;
204
+ }
205
+ transport;
206
+ accessToken;
207
+ async request(method, path, options = {}) {
208
+ const headers = { Authorization: `Bearer ${this.accessToken}` };
209
+ let body;
210
+ if (options.json !== void 0) {
211
+ headers["Content-Type"] = "application/json";
212
+ body = JSON.stringify(options.json);
213
+ }
214
+ const response = await this.transport.send(method, `/api/2${path}`, {
215
+ query: options.query,
216
+ headers,
217
+ body
218
+ });
219
+ const parsed = parseBody(response.text);
220
+ if (isRedirect(response.status)) {
221
+ throw new APIError(
222
+ `Unexpected redirect (status ${response.status}); the request was not followed`,
223
+ response.status,
224
+ parsed
225
+ );
226
+ }
227
+ if (!response.ok) throw apiErrorFor(response.status, parsed);
228
+ if (options.expectJson === false) return void 0;
229
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
230
+ throw new APIError("Expected a JSON object in the response", response.status, parsed);
231
+ }
232
+ return parsed;
233
+ }
234
+ };
235
+ function isRedirect(status) {
236
+ return status === 0 || status >= 300 && status < 400;
237
+ }
238
+ function canSetUserAgent() {
239
+ return typeof globalThis.document === "undefined";
240
+ }
241
+
242
+ // src/resources/bookmarks.ts
243
+ var MAX_PAGE_SIZE = 500;
244
+ var SECTIONS = ["home", "archive", "liked", "folder", "tag"];
245
+ var Bookmarks = class {
246
+ constructor(api) {
247
+ this.api = api;
248
+ }
249
+ api;
250
+ /** One page of bookmarks from a section. */
251
+ async list(options = {}) {
252
+ checkSection(options);
253
+ if (options.limit !== void 0) checkPageSize(options.limit, "limit");
254
+ return this.api.request("GET", "/bookmarks", {
255
+ query: {
256
+ section: options.section,
257
+ folder_id: options.folderId,
258
+ tag: options.tag,
259
+ limit: options.limit,
260
+ offset: options.offset
261
+ }
262
+ });
263
+ }
264
+ /** Every bookmark in a section, fetching pages as you go. */
265
+ async *iterate(options = {}) {
266
+ const pageSize = options.pageSize ?? 100;
267
+ checkPageSize(pageSize, "pageSize");
268
+ checkSection(options);
269
+ let offset = 0;
270
+ for (; ; ) {
271
+ const page = await this.list({
272
+ section: options.section,
273
+ folderId: options.folderId,
274
+ tag: options.tag,
275
+ limit: pageSize,
276
+ offset
277
+ });
278
+ yield* page.bookmarks;
279
+ offset += page.bookmarks.length;
280
+ if (page.bookmarks.length < pageSize) return;
281
+ }
282
+ }
283
+ /** One page of bookmarks changed since a time, across every section, plus deleted IDs. */
284
+ async changes(since, options = {}) {
285
+ const limit = options.limit ?? MAX_PAGE_SIZE;
286
+ checkPageSize(limit, "limit");
287
+ const response = await this.api.request("GET", "/bookmarks", {
288
+ query: { since: toTimestamp(since), limit, offset: options.offset }
289
+ });
290
+ return {
291
+ bookmarks: response.bookmarks,
292
+ deleted_ids: response.deleted_ids ?? [],
293
+ total: response.total
294
+ };
295
+ }
296
+ /**
297
+ * Everything changed since a time, fetching every page. Record when you
298
+ * started the sync and pass it as `since` next time.
299
+ */
300
+ async sync(since, options = {}) {
301
+ const pageSize = options.pageSize ?? MAX_PAGE_SIZE;
302
+ checkPageSize(pageSize, "pageSize");
303
+ const result = { bookmarks: [], deleted_ids: [], total: 0 };
304
+ let offset = 0;
305
+ for (; ; ) {
306
+ const page = await this.changes(since, { limit: pageSize, offset });
307
+ result.bookmarks.push(...page.bookmarks);
308
+ result.deleted_ids.push(...page.deleted_ids);
309
+ result.total = page.total;
310
+ const received = page.bookmarks.length + page.deleted_ids.length;
311
+ offset += received;
312
+ if (received < pageSize) return result;
313
+ }
314
+ }
315
+ /** Save a URL, or private content. Saving a URL twice updates the existing bookmark. */
316
+ async save(options) {
317
+ if (options.url === void 0 && options.privateSource === void 0) {
318
+ throw new TypeError("url is required unless privateSource is set");
319
+ }
320
+ if (options.url !== void 0 && options.privateSource !== void 0) {
321
+ throw new TypeError("url and privateSource cannot be used together");
322
+ }
323
+ if (options.privateSource !== void 0 && options.content === void 0) {
324
+ throw new TypeError("privateSource requires content");
325
+ }
326
+ return this.api.request("POST", "/bookmarks", {
327
+ json: compact({
328
+ url: options.url,
329
+ title: options.title,
330
+ description: options.description,
331
+ folder_id: options.folderId,
332
+ archived: options.archived,
333
+ canonicalize: options.canonicalize,
334
+ tags: options.tags?.map((name) => ({ name })),
335
+ content: options.content,
336
+ private_source: options.privateSource
337
+ })
338
+ });
339
+ }
340
+ /** Change a bookmark's title, description, or reading progress. */
341
+ async update(bookmarkId, options) {
342
+ if (options.title === void 0 && options.description === void 0 && options.progress === void 0 && options.progressTimestamp === void 0) {
343
+ throw new TypeError("update needs a title, description, or progress");
344
+ }
345
+ if (options.progress === void 0 && options.progressTimestamp !== void 0) {
346
+ throw new TypeError("progressTimestamp requires progress");
347
+ }
348
+ let progress;
349
+ if (options.progress !== void 0) {
350
+ if (!(options.progress >= 0 && options.progress <= 1)) {
351
+ throw new RangeError("progress must be between 0 and 1");
352
+ }
353
+ progress = {
354
+ percentage: options.progress,
355
+ timestamp: options.progressTimestamp ?? Math.floor(Date.now() / 1e3)
356
+ };
357
+ }
358
+ return this.api.request("POST", `/bookmarks/${id(bookmarkId)}`, {
359
+ json: compact({ title: options.title, description: options.description, progress })
360
+ });
361
+ }
362
+ /** Permanently delete a bookmark. This is not the same as archiving. */
363
+ async delete(bookmarkId) {
364
+ await this.api.request("DELETE", `/bookmarks/${id(bookmarkId)}`, { expectJson: false });
365
+ }
366
+ archive(bookmarkId) {
367
+ return this.move(bookmarkId, "archive");
368
+ }
369
+ /** Move a bookmark back to the home list. */
370
+ unarchive(bookmarkId) {
371
+ return this.move(bookmarkId, "home");
372
+ }
373
+ moveToFolder(bookmarkId, folderId) {
374
+ return this.move(bookmarkId, String(id(folderId)));
375
+ }
376
+ like(bookmarkId) {
377
+ return this.api.request("POST", `/bookmarks/${id(bookmarkId)}/like`);
378
+ }
379
+ unlike(bookmarkId) {
380
+ return this.api.request("DELETE", `/bookmarks/${id(bookmarkId)}/like`);
381
+ }
382
+ /** Add tags (by name or ID) to a bookmark and remove tags (by ID) from it. */
383
+ async updateTags(bookmarkId, options) {
384
+ const add = options.add ?? [];
385
+ const remove = options.remove ?? [];
386
+ if (add.length === 0 && remove.length === 0) {
387
+ throw new TypeError("updateTags needs at least one tag to add or remove");
388
+ }
389
+ for (const tag of add) {
390
+ const validName = typeof tag === "string" && tag.trim() !== "";
391
+ if (!validName && !isPositiveInteger(tag)) {
392
+ throw new TypeError("Tags to add must be non-empty names or positive integer IDs");
393
+ }
394
+ }
395
+ if (!remove.every(isPositiveInteger)) {
396
+ throw new TypeError("Tags to remove must be positive integer IDs");
397
+ }
398
+ return this.api.request("POST", `/bookmarks/${id(bookmarkId)}/tags`, {
399
+ json: {
400
+ add_tags: add.map((tag) => typeof tag === "number" ? { id: tag } : { name: tag }),
401
+ remove_tags: remove.map((tagId) => ({ id: tagId }))
402
+ }
403
+ });
404
+ }
405
+ /** Instapaper's parsed, reader-ready version of a saved article. */
406
+ async parse(bookmarkId, options = {}) {
407
+ const path = `/bookmarks/${id(bookmarkId)}/parse`;
408
+ const useCache = options.useCache ?? true;
409
+ const force = options.force ?? false;
410
+ if (options.content !== void 0) {
411
+ return this.api.request("POST", path, {
412
+ json: compact({
413
+ use_cache: useCache,
414
+ force,
415
+ content: options.content,
416
+ instaparser_api_key: options.instaparserApiKey
417
+ })
418
+ });
419
+ }
420
+ return this.api.request("GET", path, {
421
+ query: {
422
+ use_cache: useCache ? void 0 : "0",
423
+ force: force ? "1" : void 0,
424
+ instaparser_api_key: options.instaparserApiKey
425
+ }
426
+ });
427
+ }
428
+ move(bookmarkId, section) {
429
+ return this.api.request("POST", `/bookmarks/${id(bookmarkId)}/move`, {
430
+ json: { section }
431
+ });
432
+ }
433
+ };
434
+ function checkSection(options) {
435
+ const { section, folderId, tag } = options;
436
+ if (folderId !== void 0 && tag !== void 0) {
437
+ throw new TypeError("folderId and tag cannot be used together");
438
+ }
439
+ if (section === void 0) return;
440
+ if (!SECTIONS.includes(section)) {
441
+ throw new TypeError(`section must be one of: ${SECTIONS.join(", ")}`);
442
+ }
443
+ if (folderId !== void 0 && section !== "folder") {
444
+ throw new TypeError(`folderId can't be used with section "${section}"`);
445
+ }
446
+ if (tag !== void 0 && section !== "tag") {
447
+ throw new TypeError(`tag can't be used with section "${section}"`);
448
+ }
449
+ }
450
+ function isPositiveInteger(value) {
451
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
452
+ }
453
+ function checkPageSize(value, name) {
454
+ if (!Number.isInteger(value) || value < 1 || value > MAX_PAGE_SIZE) {
455
+ throw new RangeError(`${name} must be an integer from 1 to ${MAX_PAGE_SIZE}`);
456
+ }
457
+ }
458
+ function toTimestamp(since) {
459
+ const value = since instanceof Date ? Math.floor(since.getTime() / 1e3) : since;
460
+ if (!Number.isInteger(value) || value <= 0) {
461
+ throw new RangeError("since must be a positive Unix timestamp or a Date");
462
+ }
463
+ return value;
464
+ }
465
+ function id(value) {
466
+ if (!Number.isInteger(value) || value <= 0) {
467
+ throw new TypeError(`Expected a positive integer ID, got ${String(value)}`);
468
+ }
469
+ return value;
470
+ }
471
+ function compact(value) {
472
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
473
+ }
474
+
475
+ // src/resources/folders.ts
476
+ var Folders = class {
477
+ constructor(api) {
478
+ this.api = api;
479
+ }
480
+ api;
481
+ /** The user's folders, in their own order. */
482
+ async list() {
483
+ const response = await this.api.request("GET", "/folders");
484
+ return response.folders;
485
+ }
486
+ create(title) {
487
+ return this.api.request("POST", "/folders", { json: { title } });
488
+ }
489
+ /** Delete a folder. Its bookmarks move back to the home list. */
490
+ async delete(folderId) {
491
+ await this.api.request("DELETE", `/folders/${id(folderId)}`, { expectJson: false });
492
+ }
493
+ /**
494
+ * Set folder positions, as a `{ folderId: position }` map or a list. Folders
495
+ * you leave out keep their positions. Returns every folder in its new order.
496
+ */
497
+ async reorder(positions) {
498
+ const entries = Array.isArray(positions) ? positions : Object.entries(positions).map(([folderId, position]) => ({
499
+ folderId: Number(folderId),
500
+ position
501
+ }));
502
+ if (entries.length === 0) throw new TypeError("reorder needs at least one folder");
503
+ const order = entries.map(({ folderId, position }) => {
504
+ if (!Number.isInteger(position) || position < 1) {
505
+ throw new RangeError("Folder positions must be positive integers");
506
+ }
507
+ return { folder_id: id(folderId), position };
508
+ });
509
+ const response = await this.api.request("POST", "/folders/reorder", {
510
+ json: { order }
511
+ });
512
+ return response.folders;
513
+ }
514
+ };
515
+
516
+ // src/resources/highlights.ts
517
+ var Highlights = class {
518
+ constructor(api) {
519
+ this.api = api;
520
+ }
521
+ api;
522
+ async list(bookmarkId) {
523
+ const response = await this.api.request(
524
+ "GET",
525
+ `/bookmarks/${id(bookmarkId)}/highlights`
526
+ );
527
+ return response.highlights;
528
+ }
529
+ /**
530
+ * Create a highlight. Accounts without Premium can create five per month;
531
+ * past that this throws a `PermissionDeniedError`.
532
+ */
533
+ async create(bookmarkId, options) {
534
+ if (typeof options?.text !== "string" || options.text.trim() === "") {
535
+ throw new TypeError("Highlight text cannot be blank");
536
+ }
537
+ return this.api.request("POST", `/bookmarks/${id(bookmarkId)}/highlights`, {
538
+ json: compact({ text: options.text, note: options.note, position: options.position })
539
+ });
540
+ }
541
+ /**
542
+ * Delete a highlight. Throws `BadRequestError` if the highlight doesn't
543
+ * exist, belongs to someone else, or was already deleted.
544
+ */
545
+ async delete(highlightId) {
546
+ await this.api.request("DELETE", `/highlights/${id(highlightId)}`, {
547
+ expectJson: false
548
+ });
549
+ }
550
+ };
551
+
552
+ // src/resources/tags.ts
553
+ var Tags = class {
554
+ constructor(api) {
555
+ this.api = api;
556
+ }
557
+ api;
558
+ async list() {
559
+ const response = await this.api.request("GET", "/tags");
560
+ return response.tags;
561
+ }
562
+ create(name) {
563
+ return this.api.request("POST", "/tags", { json: { name } });
564
+ }
565
+ rename(tagId, name) {
566
+ return this.api.request("POST", `/tags/${id(tagId)}`, { json: { name } });
567
+ }
568
+ };
569
+
570
+ // src/client.ts
571
+ var Instapaper = class {
572
+ bookmarks;
573
+ folders;
574
+ tags;
575
+ highlights;
576
+ api;
577
+ constructor(options) {
578
+ if (!options || typeof options.accessToken !== "string" || !options.accessToken) {
579
+ throw new TypeError("accessToken is required");
580
+ }
581
+ this.api = new ApiClient(new Transport(options), options.accessToken);
582
+ this.bookmarks = new Bookmarks(this.api);
583
+ this.folders = new Folders(this.api);
584
+ this.tags = new Tags(this.api);
585
+ this.highlights = new Highlights(this.api);
586
+ }
587
+ /** The account the access token belongs to. */
588
+ me() {
589
+ return this.api.request("GET", "/me");
590
+ }
591
+ };
592
+
593
+ // src/oauth.ts
594
+ var OAuth = class {
595
+ clientId;
596
+ redirectUri;
597
+ clientSecret;
598
+ transport;
599
+ constructor(options) {
600
+ for (const key of ["clientId", "clientSecret", "redirectUri"]) {
601
+ if (!options || typeof options[key] !== "string" || !options[key]) {
602
+ throw new TypeError(`${key} is required`);
603
+ }
604
+ }
605
+ this.clientId = options.clientId;
606
+ this.clientSecret = options.clientSecret;
607
+ this.redirectUri = options.redirectUri;
608
+ this.transport = new Transport(options);
609
+ }
610
+ /** The URL to send the user to so they can authorize your application. */
611
+ authorizationUrl(options = {}) {
612
+ return this.transport.url("/oauth2/authorize", {
613
+ client_id: this.clientId,
614
+ redirect_uri: this.redirectUri,
615
+ response_type: "code",
616
+ state: options.state
617
+ });
618
+ }
619
+ /** Exchange the code from your redirect URI for an access token. Codes work once. */
620
+ async exchangeCode(code) {
621
+ if (typeof code !== "string" || !code) throw new TypeError("code is required");
622
+ const response = await this.transport.send("POST", "/oauth2/token", {
623
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
624
+ body: new URLSearchParams({
625
+ client_id: this.clientId,
626
+ client_secret: this.clientSecret,
627
+ redirect_uri: this.redirectUri,
628
+ code
629
+ }).toString()
630
+ });
631
+ const body = parseBody(response.text);
632
+ if (isRedirect(response.status)) {
633
+ throw new OAuthError(
634
+ response.status,
635
+ "unexpected_redirect",
636
+ `The token endpoint redirected (status ${response.status}); the request was not followed`
637
+ );
638
+ }
639
+ if (!response.ok) {
640
+ const fields = body && typeof body === "object" ? body : {};
641
+ const error = typeof fields.error === "string" ? fields.error : "unknown_error";
642
+ const description = typeof fields.error_description === "string" ? fields.error_description : `Token request failed with status ${response.status}`;
643
+ throw new OAuthError(response.status, error, description);
644
+ }
645
+ const token = body;
646
+ if (!token || typeof token !== "object" || typeof token.access_token !== "string") {
647
+ throw new InstapaperError("The token response did not include an access_token");
648
+ }
649
+ return token;
650
+ }
651
+ };
652
+
653
+ // src/types.ts
654
+ var BookmarkCategory = {
655
+ Article: 0,
656
+ Email: 1,
657
+ Video: 2,
658
+ PDF: 3,
659
+ Social: 4
660
+ };
661
+
662
+ export { APIError, AuthenticationError, BadRequestError, BookmarkCategory, Instapaper, InstapaperConnectionError, InstapaperError, NotFoundError, OAuth, OAuthError, PermissionDeniedError, QuotaExceededError, RateLimitError, ServerError, VERSION };
663
+ //# sourceMappingURL=index.js.map
664
+ //# sourceMappingURL=index.js.map