rmapi-js 14.1.0 → 14.3.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.d.ts CHANGED
@@ -86,6 +86,18 @@ export declare class ResponseError extends Error {
86
86
  readonly statusText: string;
87
87
  constructor(status: number, statusText: string, message: string);
88
88
  }
89
+ /** an error that results from the service rejecting a registration */
90
+ export declare class RegisterError extends ResponseError {
91
+ /** the response body */
92
+ readonly body: string;
93
+ constructor(status: number, statusText: string, body: string);
94
+ }
95
+ /** an error that results from the service refusing to issue a session token */
96
+ export declare class AuthError extends ResponseError {
97
+ /** the response body */
98
+ readonly body: string;
99
+ constructor(status: number, statusText: string, body: string);
100
+ }
89
101
  /** options for registering with the api */
90
102
  export interface RegisterOptions {
91
103
  /**
@@ -110,6 +122,7 @@ export interface RegisterOptions {
110
122
  * token to use the api.
111
123
  *
112
124
  * @param code - the eight letter code a user got from `https://my.remarkable.com/device/browser/connect`.
125
+ * @throws RegisterError if the service rejects the registration
113
126
  * @returns the device token necessary for creating an api instace. These never expire so persist as long as necessary.
114
127
  */
115
128
  export declare function register(code: string, { deviceDesc, uuid, authHost, }?: RegisterOptions): Promise<string>;
@@ -667,6 +680,28 @@ declare class Remarkable {
667
680
  * @returns a reference to the deleted entry, with its new hash
668
681
  */
669
682
  delete(ref: ItemRef, refresh?: boolean): Promise<ItemRef>;
683
+ /**
684
+ * permanently delete an entry
685
+ *
686
+ * Unlike {@link delete | `delete`}, which moves an entry to the trash where
687
+ * the device can still restore it, this drops the entry from the account
688
+ * outright. Its files stay in the cloud, but nothing points at them anymore
689
+ * and nothing brings the entry back.
690
+ *
691
+ * Only the entry named goes: purging a folder leaves everything inside it
692
+ * pointing at a parent that's no longer there. Those entries stay in the
693
+ * account and {@link listItems | `listItems`} still returns them, but no
694
+ * folder holds them, so nothing browsing the tree will find them. Purge the
695
+ * contents first, or use {@link purgeTrash | `purgeTrash`}, which takes the
696
+ * whole tree.
697
+ *
698
+ * @example
699
+ * ```ts
700
+ * await api.purge(file);
701
+ * ```
702
+ * @param ref - a reference to the entry to purge
703
+ */
704
+ purge(ref: ItemRef, refresh?: boolean): Promise<void>;
670
705
  /**
671
706
  * rename an entry
672
707
  *
@@ -716,6 +751,41 @@ declare class Remarkable {
716
751
  * @returns references to the deleted entries, each with its new hash
717
752
  */
718
753
  bulkDelete(refs: readonly ItemRef[], refresh?: boolean): Promise<ItemRef[]>;
754
+ /**
755
+ * permanently delete many entries
756
+ *
757
+ * The bulk form of {@link purge | `purge`}, done in a single root write.
758
+ *
759
+ * @example
760
+ * ```ts
761
+ * await api.bulkPurge([file]);
762
+ * ```
763
+ *
764
+ * @param refs - references to the entries to purge
765
+ */
766
+ bulkPurge(refs: readonly ItemRef[], refresh?: boolean): Promise<void>;
767
+ /**
768
+ * permanently delete everything in the trash
769
+ *
770
+ * Trashing a folder doesn't touch what's inside it — those entries keep
771
+ * naming the folder as their parent, which is what lets the device restore
772
+ * them together — so the trash holds the whole tree hanging off it, not just
773
+ * the entries whose parent is "trash". This purges all of it in one root
774
+ * write.
775
+ *
776
+ * @example
777
+ * ```ts
778
+ * await api.purgeTrash();
779
+ * ```
780
+ *
781
+ * @remarks
782
+ * Finding that tree means reading every item's metadata, so this costs about
783
+ * as much as {@link listItems | `listItems`}.
784
+ *
785
+ * @param refresh - if true, refresh the root hash before purging
786
+ * @returns references to the entries that were purged
787
+ */
788
+ purgeTrash(refresh?: boolean): Promise<ItemRef[]>;
719
789
  /**
720
790
  * listen for sync notifications
721
791
  *
@@ -855,6 +925,7 @@ export interface RemarkableOptions extends AuthOptions, RemarkableSessionOptions
855
925
  *
856
926
  * @param deviceToken - the device token proving this api instance is
857
927
  * registered. Create one with {@link register}.
928
+ * @throws AuthError if the service refuses to issue a session token
858
929
  * @returns the session token returned by the reMarkable service
859
930
  */
860
931
  export declare function auth(deviceToken: string, { authHost }?: AuthOptions): Promise<string>;
package/dist/index.js CHANGED
@@ -205,6 +205,24 @@ export class ResponseError extends Error {
205
205
  this.statusText = statusText;
206
206
  }
207
207
  }
208
+ /** an error that results from the service rejecting a registration */
209
+ export class RegisterError extends ResponseError {
210
+ /** the response body */
211
+ body;
212
+ constructor(status, statusText, body) {
213
+ super(status, statusText, "couldn't register api");
214
+ this.body = body;
215
+ }
216
+ }
217
+ /** an error that results from the service refusing to issue a session token */
218
+ export class AuthError extends ResponseError {
219
+ /** the response body */
220
+ body;
221
+ constructor(status, statusText, body) {
222
+ super(status, statusText, "couldn't fetch auth token");
223
+ this.body = body;
224
+ }
225
+ }
208
226
  /**
209
227
  * register a device and get the token needed to access the api
210
228
  *
@@ -213,6 +231,7 @@ export class ResponseError extends Error {
213
231
  * token to use the api.
214
232
  *
215
233
  * @param code - the eight letter code a user got from `https://my.remarkable.com/device/browser/connect`.
234
+ * @throws RegisterError if the service rejects the registration
216
235
  * @returns the device token necessary for creating an api instace. These never expire so persist as long as necessary.
217
236
  */
218
237
  export async function register(code, { deviceDesc = "browser-chrome", uuid = uuid4(), authHost = AUTH_HOST, } = {}) {
@@ -223,6 +242,7 @@ export async function register(code, { deviceDesc = "browser-chrome", uuid = uui
223
242
  method: "POST",
224
243
  headers: {
225
244
  Authorization: "Bearer",
245
+ "Content-Type": "application/json",
226
246
  },
227
247
  body: JSON.stringify({
228
248
  code,
@@ -230,11 +250,12 @@ export async function register(code, { deviceDesc = "browser-chrome", uuid = uui
230
250
  deviceID: uuid,
231
251
  }),
232
252
  });
253
+ const body = await resp.text();
233
254
  if (!resp.ok) {
234
- throw new ResponseError(resp.status, resp.statusText, "couldn't register api");
255
+ throw new RegisterError(resp.status, resp.statusText, body);
235
256
  }
236
257
  else {
237
- return await resp.text();
258
+ return body;
238
259
  }
239
260
  }
240
261
  const tokenClaims = z
@@ -407,26 +428,35 @@ class Remarkable {
407
428
  hash: rootHash,
408
429
  });
409
430
  entries.push(entry);
410
- let newRoot;
411
- {
412
- const env_2 = { stack: [], error: void 0, hasError: false };
413
- try {
414
- const rootEntry = __addDisposableResource(env_2, await this.raw.putEntries(ROOT_LIST, entries, 4), true);
415
- newRoot = rootEntry.hash;
416
- }
417
- catch (e_2) {
418
- env_2.error = e_2;
419
- env_2.hasError = true;
420
- }
421
- finally {
422
- const result_2 = __disposeResources(env_2);
423
- if (result_2)
424
- await result_2;
425
- }
426
- }
427
- await this.#putRootHash(newRoot, generation);
431
+ await this.#writeRoot(entries, generation);
428
432
  });
429
433
  }
434
+ /**
435
+ * upload a new root index and point the account at it
436
+ *
437
+ * Every blob the index names must already be uploaded, since the root hash
438
+ * lands as soon as the index does.
439
+ */
440
+ async #writeRoot(entries, generation) {
441
+ let newRoot;
442
+ {
443
+ const env_2 = { stack: [], error: void 0, hasError: false };
444
+ try {
445
+ const rootEntry = __addDisposableResource(env_2, await this.raw.putEntries(ROOT_LIST, entries, 4), true);
446
+ newRoot = rootEntry.hash;
447
+ }
448
+ catch (e_2) {
449
+ env_2.error = e_2;
450
+ env_2.hasError = true;
451
+ }
452
+ finally {
453
+ const result_2 = __disposeResources(env_2);
454
+ if (result_2)
455
+ await result_2;
456
+ }
457
+ }
458
+ await this.#putRootHash(newRoot, generation);
459
+ }
430
460
  async #authedFetch(url, { body, method = "POST", headers = {}, }) {
431
461
  // the root PUT is a compare-and-set; retrying a lost-but-applied response
432
462
  // would resurface as a false generation conflict and be double-applied by
@@ -1517,6 +1547,30 @@ class Remarkable {
1517
1547
  async delete(ref, refresh = false) {
1518
1548
  return await this.move(ref, TRASH_ID, refresh);
1519
1549
  }
1550
+ /**
1551
+ * permanently delete an entry
1552
+ *
1553
+ * Unlike {@link delete | `delete`}, which moves an entry to the trash where
1554
+ * the device can still restore it, this drops the entry from the account
1555
+ * outright. Its files stay in the cloud, but nothing points at them anymore
1556
+ * and nothing brings the entry back.
1557
+ *
1558
+ * Only the entry named goes: purging a folder leaves everything inside it
1559
+ * pointing at a parent that's no longer there. Those entries stay in the
1560
+ * account and {@link listItems | `listItems`} still returns them, but no
1561
+ * folder holds them, so nothing browsing the tree will find them. Purge the
1562
+ * contents first, or use {@link purgeTrash | `purgeTrash`}, which takes the
1563
+ * whole tree.
1564
+ *
1565
+ * @example
1566
+ * ```ts
1567
+ * await api.purge(file);
1568
+ * ```
1569
+ * @param ref - a reference to the entry to purge
1570
+ */
1571
+ async purge(ref, refresh = false) {
1572
+ await this.bulkPurge([ref], refresh);
1573
+ }
1520
1574
  /**
1521
1575
  * rename an entry
1522
1576
  *
@@ -1631,6 +1685,118 @@ class Remarkable {
1631
1685
  async bulkDelete(refs, refresh = false) {
1632
1686
  return await this.bulkMove(refs, TRASH_ID, refresh);
1633
1687
  }
1688
+ /**
1689
+ * permanently delete many entries
1690
+ *
1691
+ * The bulk form of {@link purge | `purge`}, done in a single root write.
1692
+ *
1693
+ * @example
1694
+ * ```ts
1695
+ * await api.bulkPurge([file]);
1696
+ * ```
1697
+ *
1698
+ * @param refs - references to the entries to purge
1699
+ */
1700
+ async bulkPurge(refs, refresh = false) {
1701
+ if (!refs.length) {
1702
+ return;
1703
+ }
1704
+ await this.#withRetry(async () => {
1705
+ const [rootHash, generation] = await this.#getRootHash(refresh);
1706
+ const { entries } = await this.raw.getEntries({
1707
+ id: ROOT_LIST,
1708
+ hash: rootHash,
1709
+ });
1710
+ const wanted = new Set(refs.map((ref) => `${ref.id}\0${ref.hash}`));
1711
+ const found = new Set();
1712
+ const newEntries = [];
1713
+ for (const entry of entries) {
1714
+ const key = `${entry.id}\0${entry.hash}`;
1715
+ if (wanted.has(key)) {
1716
+ found.add(key);
1717
+ }
1718
+ else {
1719
+ newEntries.push(entry);
1720
+ }
1721
+ }
1722
+ for (const ref of refs) {
1723
+ if (!found.has(`${ref.id}\0${ref.hash}`)) {
1724
+ throw new HashNotFoundError(ref.hash);
1725
+ }
1726
+ }
1727
+ await this.#writeRoot(newEntries, generation);
1728
+ });
1729
+ }
1730
+ /**
1731
+ * permanently delete everything in the trash
1732
+ *
1733
+ * Trashing a folder doesn't touch what's inside it — those entries keep
1734
+ * naming the folder as their parent, which is what lets the device restore
1735
+ * them together — so the trash holds the whole tree hanging off it, not just
1736
+ * the entries whose parent is "trash". This purges all of it in one root
1737
+ * write.
1738
+ *
1739
+ * @example
1740
+ * ```ts
1741
+ * await api.purgeTrash();
1742
+ * ```
1743
+ *
1744
+ * @remarks
1745
+ * Finding that tree means reading every item's metadata, so this costs about
1746
+ * as much as {@link listItems | `listItems`}.
1747
+ *
1748
+ * @param refresh - if true, refresh the root hash before purging
1749
+ * @returns references to the entries that were purged
1750
+ */
1751
+ async purgeTrash(refresh = false) {
1752
+ return await this.#withRetry(async () => {
1753
+ const [rootHash, generation] = await this.#getRootHash(refresh);
1754
+ const { entries } = await this.raw.getEntries({
1755
+ id: ROOT_LIST,
1756
+ hash: rootHash,
1757
+ });
1758
+ const parents = await Promise.all(entries.map(async (entry) => (await this.getMetadata(entry)).parent));
1759
+ const children = new Map();
1760
+ for (const [ind, entry] of entries.entries()) {
1761
+ const siblings = children.get(parents[ind]);
1762
+ if (siblings === undefined) {
1763
+ children.set(parents[ind], [entry]);
1764
+ }
1765
+ else {
1766
+ siblings.push(entry);
1767
+ }
1768
+ }
1769
+ const trashed = new Set();
1770
+ let frontier = children.get(TRASH_ID) ?? [];
1771
+ while (frontier.length) {
1772
+ const next = [];
1773
+ for (const { id } of frontier) {
1774
+ // skipping what we've already seen also ends a parent cycle, which
1775
+ // the account shouldn't have but which would loop here forever
1776
+ if (!trashed.has(id)) {
1777
+ trashed.add(id);
1778
+ next.push(...(children.get(id) ?? []));
1779
+ }
1780
+ }
1781
+ frontier = next;
1782
+ }
1783
+ if (!trashed.size) {
1784
+ return [];
1785
+ }
1786
+ const purged = [];
1787
+ const newEntries = [];
1788
+ for (const entry of entries) {
1789
+ if (trashed.has(entry.id)) {
1790
+ purged.push({ id: entry.id, hash: entry.hash });
1791
+ }
1792
+ else {
1793
+ newEntries.push(entry);
1794
+ }
1795
+ }
1796
+ await this.#writeRoot(newEntries, generation);
1797
+ return purged;
1798
+ });
1799
+ }
1634
1800
  /**
1635
1801
  * listen for sync notifications
1636
1802
  *
@@ -1840,6 +2006,7 @@ function decodeCache(dumped) {
1840
2006
  *
1841
2007
  * @param deviceToken - the device token proving this api instance is
1842
2008
  * registered. Create one with {@link register}.
2009
+ * @throws AuthError if the service refuses to issue a session token
1843
2010
  * @returns the session token returned by the reMarkable service
1844
2011
  */
1845
2012
  export async function auth(deviceToken, { authHost = AUTH_HOST } = {}) {
@@ -1849,10 +2016,13 @@ export async function auth(deviceToken, { authHost = AUTH_HOST } = {}) {
1849
2016
  Authorization: `Bearer ${deviceToken}`,
1850
2017
  },
1851
2018
  });
2019
+ const body = await resp.text();
1852
2020
  if (!resp.ok) {
1853
- throw new Error(`couldn't fetch auth token: ${resp.statusText}`);
2021
+ throw new AuthError(resp.status, resp.statusText, body);
2022
+ }
2023
+ else {
2024
+ return body;
1854
2025
  }
1855
- return await resp.text();
1856
2026
  }
1857
2027
  /**
1858
2028
  * Create an API instance from an existing session token.