rmapi-js 8.2.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
  *
@@ -167,7 +176,7 @@ export interface DocumentContent {
167
176
  */
168
177
  fontName: string;
169
178
  /** the format version, this should always be 1 */
170
- formatVersion: number;
179
+ formatVersion?: number;
171
180
  /** the last opened page, starts at zero */
172
181
  lastOpenedPage?: number;
173
182
  /**
@@ -210,17 +219,37 @@ export interface DocumentContent {
210
219
  * values outside of this range are valid.
211
220
  */
212
221
  textScale: number;
213
- /** [speculative] the center of the zoom for zoomed in documents */
222
+ /**
223
+ * the center of the zoom for customFit zoom
224
+ *
225
+ * This is an absolute offset from the center of the page. Negative numbers
226
+ * indicate shifted left and positive numbers indicate shifted right. The
227
+ * units are relative to the document pixels, but it's not sure how the
228
+ * document size is calculated.
229
+ */
214
230
  customZoomCenterX?: number;
215
- /** [speculative] the center of the zoom for zoomed in documents */
231
+ /**
232
+ * the center of the zoom for customFit documents
233
+ *
234
+ * This is an absolute number relative to the top of the page. Negative
235
+ * numbers indicate shifted up, while positive numbers indicate shifted down.
236
+ * The units are relative to the document pixels, but it's not sure how the
237
+ * document size is calculated.
238
+ */
216
239
  customZoomCenterY?: number;
217
- /** [speculative] the orientation */
240
+ /** this seems unused */
218
241
  customZoomOrientation?: Orientation;
219
- /** [speculative] the zoom height for zoomed in pages */
242
+ /** this seems unused */
220
243
  customZoomPageHeight?: number;
221
- /** [speculative] the zoom width for zoomed in pages */
244
+ /** this seems unused */
222
245
  customZoomPageWidth?: number;
223
- /** [speculative] the scale for zoomed in pages */
246
+ /**
247
+ * the scale for customFit documents
248
+ *
249
+ * 1 indicates no zoom, smaller numbers indicate zoomed out, larger numbers
250
+ * indicate zoomed in. reMarkable generally allows setting this from 0.5 to 5,
251
+ * but values outside that bound are still supported.
252
+ */
224
253
  customZoomScale?: number;
225
254
  /** what zoom mode is set for the page */
226
255
  zoomMode?: ZoomMode;
@@ -261,7 +290,7 @@ export interface TemplateContent {
261
290
  /** semantic version for this template */
262
291
  templateVersion: string;
263
292
  /** template configuration format version (currently just `1`) */
264
- formatVersion: number;
293
+ formatVersion?: number;
265
294
  /**
266
295
  * which screens the template supports:
267
296
  *
@@ -498,9 +527,24 @@ export interface RawRemarkableApi {
498
527
  * @param id - the id of the list to upload - this should be the item id if
499
528
  * uploading an item list, or "root" if uploading a new root list.
500
529
  * @param entries - the entries to upload
530
+ *
501
531
  * @returns the new list entry and a promise to finish the upload
502
532
  */
503
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>;
504
548
  /**
505
549
  * dump the current cache to a string to preserve between session
506
550
  *
@@ -518,7 +562,7 @@ interface AuthedFetch {
518
562
  }
519
563
  export declare class RawRemarkable implements RawRemarkableApi {
520
564
  #private;
521
- constructor(authedFetch: AuthedFetch, cache: Map<string, string | null>, rawHost: string);
565
+ constructor(authedFetch: AuthedFetch, cache: Map<string, string | null>, rawHost: string, uploadHost: string);
522
566
  /** make an authorized request to remarkable */
523
567
  getRootHash(): Promise<[string, number]>;
524
568
  getHash(hash: string): Promise<Uint8Array>;
@@ -532,6 +576,7 @@ export declare class RawRemarkable implements RawRemarkableApi {
532
576
  putContent(id: string, content: Content): Promise<[RawFileEntry, Promise<void>]>;
533
577
  putMetadata(id: string, metadata: Metadata): Promise<[RawFileEntry, Promise<void>]>;
534
578
  putEntries(id: string, entries: RawEntry[]): Promise<[RawListEntry, Promise<void>]>;
579
+ uploadFile(visibleName: string, bytes: Uint8Array, mime: UploadMimeType): Promise<SimpleEntry>;
535
580
  dumpCache(): string;
536
581
  clearCache(): void;
537
582
  }
package/dist/raw.js CHANGED
@@ -70,7 +70,6 @@ const documentContent = properties({
70
70
  extraMetadata: values(string()),
71
71
  fileType: enumeration("epub", "notebook", "pdf"),
72
72
  fontName: string(),
73
- formatVersion: uint8(),
74
73
  lineHeight: int32(),
75
74
  orientation: enumeration("portrait", "landscape"),
76
75
  pageCount: uint32(),
@@ -86,6 +85,7 @@ const documentContent = properties({
86
85
  customZoomPageWidth: float64(),
87
86
  customZoomScale: float64(),
88
87
  dummyDocument: boolean(),
88
+ formatVersion: uint8(),
89
89
  keyboardMetadata: properties({
90
90
  count: uint32(),
91
91
  timestamp: float64(),
@@ -120,10 +120,11 @@ const templateContent = properties({
120
120
  labels: elements(string()),
121
121
  orientation: enumeration("portrait", "landscape"),
122
122
  templateVersion: string(),
123
- formatVersion: uint8(),
124
123
  supportedScreens: elements(enumeration("rm2", "rmPP")),
125
124
  constants: elements(values(int32())),
126
125
  items: elements(empty()),
126
+ }, {
127
+ formatVersion: uint8(),
127
128
  });
128
129
  const metadata = properties({
129
130
  lastModified: string(),
@@ -150,8 +151,14 @@ const rootHash = properties({
150
151
  generation: float64(),
151
152
  schemaVersion: uint8(),
152
153
  }, undefined, true);
154
+ const NativeSimpleEntry = properties({
155
+ docID: string(),
156
+ hash: string(),
157
+ }, undefined, true);
153
158
  async function digest(buff) {
154
- 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);
155
162
  return [...new Uint8Array(digest)]
156
163
  .map((x) => x.toString(16).padStart(2, "0"))
157
164
  .join("");
@@ -159,6 +166,7 @@ async function digest(buff) {
159
166
  export class RawRemarkable {
160
167
  #authedFetch;
161
168
  #rawHost;
169
+ #uploadHost;
162
170
  /**
163
171
  * a cache of all hashes we know exist
164
172
  *
@@ -169,10 +177,11 @@ export class RawRemarkable {
169
177
  * not to write a a cached value again, but we'll still need to read it.
170
178
  */
171
179
  #cache;
172
- constructor(authedFetch, cache, rawHost) {
180
+ constructor(authedFetch, cache, rawHost, uploadHost) {
173
181
  this.#authedFetch = authedFetch;
174
182
  this.#cache = cache;
175
183
  this.#rawHost = rawHost;
184
+ this.#uploadHost = uploadHost;
176
185
  }
177
186
  /** make an authorized request to remarkable */
178
187
  async getRootHash() {
@@ -417,6 +426,23 @@ export class RawRemarkable {
417
426
  this.#putFile(hash, `${id}.docSchema`, enc.encode(records.join(""))),
418
427
  ];
419
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
+ }
420
446
  dumpCache() {
421
447
  return JSON.stringify(Object.fromEntries(this.#cache));
422
448
  }