rmapi-js 8.3.0 → 8.4.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
@@ -1,6 +1,6 @@
1
- import { type BackgroundFilter, type CollectionContent, type Content, type DocumentContent, type Metadata, type Orientation, type RawRemarkableApi, type Tag, type TemplateContent, type TextAlignment, type ZoomMode } from "./raw";
1
+ import { type BackgroundFilter, type CollectionContent, type Content, type DocumentContent, type Metadata, type Orientation, type RawRemarkableApi, type SimpleEntry, type Tag, type TemplateContent, type TextAlignment, type ZoomMode } from "./raw";
2
2
  export { HashNotFoundError, ValidationError } from "./error";
3
- export type { BackgroundFilter, CollectionContent, Content, CPageNumberValue, CPagePage, CPages, CPageStringValue, CPageUUID, DocumentContent, DocumentMetadata, FileType, KeyboardMetadata, Metadata, Orientation, PageTag, RawEntry, RawFileEntry, RawListEntry, RawRemarkableApi, Tag, TemplateContent, TextAlignment, ZoomMode, } from "./raw";
3
+ export type { BackgroundFilter, CollectionContent, Content, CPageNumberValue, CPagePage, CPages, CPageStringValue, CPageUUID, DocumentContent, DocumentMetadata, FileType, KeyboardMetadata, Metadata, Orientation, PageTag, RawEntry, RawFileEntry, RawListEntry, RawRemarkableApi, SimpleEntry, Tag, TemplateContent, TextAlignment, UploadMimeType, ZoomMode, } from "./raw";
4
4
  /** common properties shared by collections and documents */
5
5
  export interface EntryCommon {
6
6
  /** the document id, a uuid4 */
@@ -50,13 +50,6 @@ export interface TemplateType extends EntryCommon {
50
50
  }
51
51
  /** a remarkable entry for cloud items */
52
52
  export type Entry = CollectionEntry | DocumentType | TemplateType;
53
- /** an simple entry without any extra information */
54
- export interface SimpleEntry {
55
- /** the document id */
56
- id: string;
57
- /** the document hash */
58
- hash: string;
59
- }
60
53
  /** the new hash of a modified entry */
61
54
  export interface HashEntry {
62
55
  /** the actual hash */
@@ -67,6 +60,11 @@ export interface HashesEntry {
67
60
  /** the mapping from old to new hashes */
68
61
  hashes: Record<string, string>;
69
62
  }
63
+ /** options for creating a folder */
64
+ export interface FolderOptions {
65
+ /** the id of the folder's parent directory, "" or omitted for root */
66
+ parent?: string;
67
+ }
70
68
  /** An error that gets thrown when the backend while trying to update
71
69
  *
72
70
  * IF you encounter this error, you likely just need to try th request again. If
@@ -114,11 +112,6 @@ export interface RegisterOptions {
114
112
  * @returns the device token necessary for creating an api instace. These never expire so persist as long as necessary.
115
113
  */
116
114
  export declare function register(code: string, { deviceDesc, uuid, authHost, }?: RegisterOptions): Promise<string>;
117
- /** options available when uploading a document */
118
- export interface UploadOptions {
119
- /** an optional parent id to set when uploading */
120
- parent?: string;
121
- }
122
115
  /**
123
116
  * options for putting a file onto reMarkable
124
117
  *
@@ -322,7 +315,7 @@ export interface RemarkableApi {
322
315
  */
323
316
  putEpub(visibleName: string, buffer: Uint8Array, opts?: PutOptions): Promise<SimpleEntry>;
324
317
  /** create a folder */
325
- createFolder(visibleName: string, opts?: UploadOptions, refresh?: boolean): Promise<SimpleEntry>;
318
+ putFolder(visibleName: string, opts?: FolderOptions, refresh?: boolean): Promise<SimpleEntry>;
326
319
  /**
327
320
  * upload an epub
328
321
  *
@@ -332,12 +325,12 @@ export interface RemarkableApi {
332
325
  * ```
333
326
  *
334
327
  * @remarks
335
- * this is now simply a less powerful version of {@link putEpub | `putEpub`}.
328
+ * this uses a simpler api that works even with schema version 4.
336
329
  *
337
330
  * @param visibleName - the name to show for the uploaded epub
338
331
  * @param buffer - the epub contents
339
332
  */
340
- uploadEpub(visibleName: string, buffer: Uint8Array, opts?: UploadOptions): Promise<SimpleEntry>;
333
+ uploadEpub(visibleName: string, buffer: Uint8Array): Promise<SimpleEntry>;
341
334
  /**
342
335
  * upload a pdf
343
336
  *
@@ -347,12 +340,14 @@ export interface RemarkableApi {
347
340
  * ```
348
341
  *
349
342
  * @remarks
350
- * this is now simply a less powerful version of {@link putPdf | `putPdf`}.
343
+ * this uses a simpler api that works even with schema version 4.
351
344
  *
352
345
  * @param visibleName - the name to show for the uploaded epub
353
346
  * @param buffer - the epub contents
354
347
  */
355
- uploadPdf(visibleName: string, buffer: Uint8Array, opts?: UploadOptions): Promise<SimpleEntry>;
348
+ uploadPdf(visibleName: string, buffer: Uint8Array): Promise<SimpleEntry>;
349
+ /** create a folder using the simple api */
350
+ uploadFolder(visibleName: string): Promise<SimpleEntry>;
356
351
  /**
357
352
  * update content metadata for a document
358
353
  *
@@ -501,6 +496,12 @@ export interface RemarkableOptions {
501
496
  * @defaultValue "https://web.eu.tectonic.remarkable.com"
502
497
  */
503
498
  syncHost?: string;
499
+ /**
500
+ * the base url for making upload requests
501
+ *
502
+ * @defaultValue "https://internal.cloud.remarkable.com"
503
+ */
504
+ uploadHost?: string;
504
505
  /**
505
506
  * the url for making requests using the low-level api
506
507
  *
@@ -535,4 +536,4 @@ export interface RemarkableOptions {
535
536
  * registered. Create one with {@link register}.
536
537
  * @returns an api instance
537
538
  */
538
- export declare function remarkable(deviceToken: string, { authHost, rawHost, cache, maxCacheSize, }?: RemarkableOptions): Promise<RemarkableApi>;
539
+ export declare function remarkable(deviceToken: string, { authHost, rawHost, uploadHost, cache, maxCacheSize, }?: RemarkableOptions): Promise<RemarkableApi>;
package/dist/index.js CHANGED
@@ -60,6 +60,7 @@ import { RawRemarkable, } from "./raw";
60
60
  export { HashNotFoundError, ValidationError } from "./error";
61
61
  const AUTH_HOST = "https://webapp-prod.cloud.remarkable.engineering";
62
62
  const RAW_HOST = "https://eu.tectonic.remarkable.com";
63
+ const UPLOAD_HOST = "https://internal.cloud.remarkable.com";
63
64
  // ------------ //
64
65
  // Request Info //
65
66
  // ------------ //
@@ -130,10 +131,10 @@ class Remarkable {
130
131
  #cache;
131
132
  raw;
132
133
  #lastHashGen;
133
- constructor(userToken, rawHost, cache) {
134
+ constructor(userToken, rawHost, uploadHost, cache) {
134
135
  this.#userToken = userToken;
135
136
  this.#cache = cache;
136
- this.raw = new RawRemarkable((method, url, { body, headers } = {}) => this.#authedFetch(url, { method, body, headers }), cache, rawHost);
137
+ this.raw = new RawRemarkable((method, url, { body, headers } = {}) => this.#authedFetch(url, { method, body, headers }), cache, rawHost, uploadHost);
137
138
  }
138
139
  async #getRootHash(refresh = false) {
139
140
  if (refresh || this.#lastHashGen === undefined) {
@@ -160,7 +161,8 @@ class Remarkable {
160
161
  Authorization: `Bearer ${this.#userToken}`,
161
162
  ...headers,
162
163
  },
163
- body,
164
+ // fetch works correctly with uint8 arrays, but is not hinted correctly
165
+ body: body,
164
166
  });
165
167
  if (!resp.ok) {
166
168
  const msg = await resp.text();
@@ -182,12 +184,12 @@ class Remarkable {
182
184
  if (metaEnt === undefined) {
183
185
  throw new Error(`couldn't find metadata for hash ${hash}`);
184
186
  }
185
- else if (contentEnt === undefined) {
186
- throw new Error(`couldn't find content for hash ${hash}`);
187
- }
188
187
  const [{ visibleName, lastModified, pinned, parent, lastOpened, new: isNew, source, }, content,] = await Promise.all([
189
188
  this.raw.getMetadata(metaEnt.hash),
190
- this.raw.getContent(contentEnt.hash),
189
+ // collections don't always have content, since content only lists tags
190
+ contentEnt === undefined
191
+ ? Promise.resolve({ fileType: undefined, tags: undefined })
192
+ : this.raw.getContent(contentEnt.hash),
191
193
  ]);
192
194
  if ("templateVersion" in content) {
193
195
  return {
@@ -373,7 +375,7 @@ class Remarkable {
373
375
  return await this.#putFile(visibleName, "epub", buffer, opts);
374
376
  }
375
377
  /** create a folder */
376
- async createFolder(visibleName, { parent = "" } = {}, refresh = false) {
378
+ async putFolder(visibleName, { parent = "" } = {}, refresh = false) {
377
379
  if (parent && !idReg.test(parent)) {
378
380
  throw new ValidationError(parent, idReg, "parent must be a valid document id");
379
381
  }
@@ -416,12 +418,16 @@ class Remarkable {
416
418
  return { id, hash: collectionEntry.hash };
417
419
  }
418
420
  /** upload an epub */
419
- async uploadEpub(visibleName, buffer, opts = {}) {
420
- return await this.putEpub(visibleName, buffer, opts);
421
+ async uploadEpub(visibleName, buffer) {
422
+ return await this.raw.uploadFile(visibleName, buffer, "application/epub+zip");
421
423
  }
422
424
  /** upload a pdf */
423
- async uploadPdf(visibleName, buffer, opts = {}) {
424
- return await this.putPdf(visibleName, buffer, opts);
425
+ async uploadPdf(visibleName, buffer) {
426
+ return await this.raw.uploadFile(visibleName, buffer, "application/pdf");
427
+ }
428
+ /** upload a folder */
429
+ async uploadFolder(visibleName) {
430
+ return await this.raw.uploadFile(visibleName, new Uint8Array(0), "folder");
425
431
  }
426
432
  /** edit just a content entry */
427
433
  async #editContentRaw(id, hash, update) {
@@ -599,7 +605,7 @@ const cached = values(nullable(string()));
599
605
  * registered. Create one with {@link register}.
600
606
  * @returns an api instance
601
607
  */
602
- export async function remarkable(deviceToken, { authHost = AUTH_HOST, rawHost = RAW_HOST, cache, maxCacheSize = Infinity, } = {}) {
608
+ export async function remarkable(deviceToken, { authHost = AUTH_HOST, rawHost = RAW_HOST, uploadHost = UPLOAD_HOST, cache, maxCacheSize = Infinity, } = {}) {
603
609
  const resp = await fetch(`${authHost}/token/json/2/user/new`, {
604
610
  method: "POST",
605
611
  headers: {
@@ -616,7 +622,7 @@ export async function remarkable(deviceToken, { authHost = AUTH_HOST, rawHost =
616
622
  const cache = maxCacheSize === Infinity
617
623
  ? new Map(entries)
618
624
  : new LruCache(maxCacheSize, entries);
619
- return new Remarkable(userToken, rawHost, cache);
625
+ return new Remarkable(userToken, rawHost, uploadHost, cache);
620
626
  }
621
627
  else {
622
628
  throw new Error("cache was not a valid cache (json string mapping); your cache must be corrupted somehow. Either initialize remarkable without a cache, or fix its format.");
package/dist/raw.d.ts CHANGED
@@ -1,5 +1,14 @@
1
1
  /** request types */
2
2
  export type RequestMethod = "POST" | "GET" | "PUT" | "DELETE" | "PATCH" | "OPTIONS";
3
+ /** the supported upload mime types */
4
+ export type UploadMimeType = "application/pdf" | "application/epub+zip" | "folder";
5
+ /** an simple entry without any extra information */
6
+ export interface SimpleEntry {
7
+ /** the document id */
8
+ id: string;
9
+ /** the document hash */
10
+ hash: string;
11
+ }
3
12
  /**
4
13
  * the low-level entry corresponding to a collection of files
5
14
  *
@@ -518,9 +527,24 @@ export interface RawRemarkableApi {
518
527
  * @param id - the id of the list to upload - this should be the item id if
519
528
  * uploading an item list, or "root" if uploading a new root list.
520
529
  * @param entries - the entries to upload
530
+ *
521
531
  * @returns the new list entry and a promise to finish the upload
522
532
  */
523
533
  putEntries(id: string, entries: RawEntry[]): Promise<[RawListEntry, Promise<void>]>;
534
+ /**
535
+ * upload a file to the reMarkable cloud using the simple api
536
+ *
537
+ * This api is the same as used by the native reMarkable extension and works
538
+ * even if the backend schema version is version 4. Setting mime to "folder"
539
+ * allows folder creation.
540
+ *
541
+ * @param visibleName - the name of the file as it should appear on the reMarkable
542
+ * @param bytes - the bytes of the file to upload
543
+ * @param mime - the mime type of the file to upload
544
+
545
+ * @returns a simple entry with the id and hash of the uploaded file
546
+ */
547
+ uploadFile(visibleName: string, bytes: Uint8Array, mime: UploadMimeType): Promise<SimpleEntry>;
524
548
  /**
525
549
  * dump the current cache to a string to preserve between session
526
550
  *
@@ -538,7 +562,7 @@ interface AuthedFetch {
538
562
  }
539
563
  export declare class RawRemarkable implements RawRemarkableApi {
540
564
  #private;
541
- constructor(authedFetch: AuthedFetch, cache: Map<string, string | null>, rawHost: string);
565
+ constructor(authedFetch: AuthedFetch, cache: Map<string, string | null>, rawHost: string, uploadHost: string);
542
566
  /** make an authorized request to remarkable */
543
567
  getRootHash(): Promise<[string, number]>;
544
568
  getHash(hash: string): Promise<Uint8Array>;
@@ -552,6 +576,7 @@ export declare class RawRemarkable implements RawRemarkableApi {
552
576
  putContent(id: string, content: Content): Promise<[RawFileEntry, Promise<void>]>;
553
577
  putMetadata(id: string, metadata: Metadata): Promise<[RawFileEntry, Promise<void>]>;
554
578
  putEntries(id: string, entries: RawEntry[]): Promise<[RawListEntry, Promise<void>]>;
579
+ uploadFile(visibleName: string, bytes: Uint8Array, mime: UploadMimeType): Promise<SimpleEntry>;
555
580
  dumpCache(): string;
556
581
  clearCache(): void;
557
582
  }
package/dist/raw.js CHANGED
@@ -151,8 +151,14 @@ const rootHash = properties({
151
151
  generation: float64(),
152
152
  schemaVersion: uint8(),
153
153
  }, undefined, true);
154
+ const NativeSimpleEntry = properties({
155
+ docID: string(),
156
+ hash: string(),
157
+ }, undefined, true);
154
158
  async function digest(buff) {
155
- const digest = await crypto.subtle.digest("SHA-256", buff);
159
+ const digest = await crypto.subtle.digest("SHA-256",
160
+ // NOTE this is type hinted wrong, but it does work correctly on a uint8 view
161
+ buff);
156
162
  return [...new Uint8Array(digest)]
157
163
  .map((x) => x.toString(16).padStart(2, "0"))
158
164
  .join("");
@@ -160,6 +166,7 @@ async function digest(buff) {
160
166
  export class RawRemarkable {
161
167
  #authedFetch;
162
168
  #rawHost;
169
+ #uploadHost;
163
170
  /**
164
171
  * a cache of all hashes we know exist
165
172
  *
@@ -170,10 +177,11 @@ export class RawRemarkable {
170
177
  * not to write a a cached value again, but we'll still need to read it.
171
178
  */
172
179
  #cache;
173
- constructor(authedFetch, cache, rawHost) {
180
+ constructor(authedFetch, cache, rawHost, uploadHost) {
174
181
  this.#authedFetch = authedFetch;
175
182
  this.#cache = cache;
176
183
  this.#rawHost = rawHost;
184
+ this.#uploadHost = uploadHost;
177
185
  }
178
186
  /** make an authorized request to remarkable */
179
187
  async getRootHash() {
@@ -418,6 +426,23 @@ export class RawRemarkable {
418
426
  this.#putFile(hash, `${id}.docSchema`, enc.encode(records.join(""))),
419
427
  ];
420
428
  }
429
+ async uploadFile(visibleName, bytes, mime) {
430
+ const enc = new TextEncoder();
431
+ const meta = fromByteArray(enc.encode(JSON.stringify({ file_name: visibleName })));
432
+ const resp = await this.#authedFetch("POST", `${this.#uploadHost}/doc/v2/files`, {
433
+ body: bytes,
434
+ headers: {
435
+ "Content-Type": mime,
436
+ "rm-meta": meta,
437
+ "rm-source": "RoR-Browser",
438
+ },
439
+ });
440
+ const loaded = (await resp.json());
441
+ if (!NativeSimpleEntry.guardAssert(loaded))
442
+ throw Error("invalid upload response");
443
+ const { docID, hash } = loaded;
444
+ return { id: docID, hash };
445
+ }
421
446
  dumpCache() {
422
447
  return JSON.stringify(Object.fromEntries(this.#cache));
423
448
  }