rmapi-js 8.3.0 → 8.5.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/README.md CHANGED
@@ -21,19 +21,28 @@ and folders ("collections"). The hash indicates the full current state to manage
21
21
  ## Usage
22
22
 
23
23
  To explore files in the cloud, you need to first register your api and persist
24
- the token. Then you can use `listFiles` to explore entries of different file
24
+ the token. Then you can use `listItems` to explore entries of different file
25
25
  collections.
26
26
 
27
27
  ```ts
28
- import { register, remarkable } from "rmapi-js";
28
+ import { auth, register, remarkable, session } from "rmapi-js";
29
29
 
30
30
  const code = "..."; // eight letter code from https://my.remarkable.com/device/desktop/connect
31
31
  const token = await register(code);
32
32
  // persist token so you don't have to register again
33
33
  const api = await remarkable(token);
34
- const fileEntries = await api.listFiles();
34
+ const fileEntries = await api.listItems();
35
+
36
+ // In stateless environments, exchange once and reuse.
37
+ const sessionToken = await auth(token);
38
+ const api = session(sessionToken);
39
+ // cache `sessionToken` and reuse it across workers
35
40
  ```
36
41
 
42
+ `auth` performs the same network call that `remarkable` does for you internally,
43
+ returning a short-lived session token. `session` is synchronous,
44
+ letting you construct clients from cached tokens without making a network call.
45
+
37
46
  To upload an epub or pdf, simply call upload with the appropriate name and buffer.
38
47
 
39
48
  ```ts
@@ -53,7 +62,7 @@ Using these apis is a little riskier since they can potentially result in data l
53
62
  // upload with custom line height not avilable through reMarkable
54
63
  await api.putEpub("name", buffer, { lineHeight: 180 })
55
64
 
56
- // fetch an uploaded pdf, using the hash (from listFiles)
65
+ // fetch an uploaded pdf, using the hash (from listItems)
57
66
  const buffer = await api.getEpub(hash)
58
67
  ```
59
68
 
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, CPageStringValue, CPages, 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
  *
@@ -487,20 +482,29 @@ export interface RemarkableApi {
487
482
  */
488
483
  clearCache(): void;
489
484
  }
490
- /** options for a remarkable instance */
491
- export interface RemarkableOptions {
485
+ /** configuration for exchanging a device token */
486
+ export interface AuthOptions {
492
487
  /**
493
488
  * the url for making authorization requests
494
489
  *
495
490
  * @defaultValue "https://webapp-prod.cloud.remarkable.engineering"
496
491
  */
497
492
  authHost?: string;
493
+ }
494
+ /** options for constructing an api instance from a session token */
495
+ export interface RemarkableSessionOptions {
498
496
  /**
499
497
  * the url for making synchronization requests
500
498
  *
501
499
  * @defaultValue "https://web.eu.tectonic.remarkable.com"
502
500
  */
503
501
  syncHost?: string;
502
+ /**
503
+ * the base url for making upload requests
504
+ *
505
+ * @defaultValue "https://internal.cloud.remarkable.com"
506
+ */
507
+ uploadHost?: string;
504
508
  /**
505
509
  * the url for making requests using the low-level api
506
510
  *
@@ -525,14 +529,35 @@ export interface RemarkableOptions {
525
529
  */
526
530
  maxCacheSize?: number;
527
531
  }
532
+ /** options for a remarkable instance */
533
+ export interface RemarkableOptions extends AuthOptions, RemarkableSessionOptions {
534
+ }
535
+ /**
536
+ * Exchange a device token for a session token.
537
+ *
538
+ * @param deviceToken - the device token proving this api instance is
539
+ * registered. Create one with {@link register}.
540
+ * @returns the session token returned by the reMarkable service
541
+ */
542
+ export declare function auth(deviceToken: string, { authHost }?: AuthOptions): Promise<string>;
543
+ /**
544
+ * Create an API instance from an existing session token.
545
+ *
546
+ * If requests start failing, simply recreate the api instance with a freshly
547
+ * fetched session token.
548
+ *
549
+ * @param sessionToken - the session token used for authorization
550
+ * @returns an api instance
551
+ */
552
+ export declare function session(sessionToken: string, { rawHost, uploadHost, cache, maxCacheSize, }?: RemarkableSessionOptions): RemarkableApi;
528
553
  /**
529
554
  * create an instance of the api
530
555
  *
531
- * This gets a temporary authentication token with the device token. If
532
- * requests start failing, simply recreate the api instance.
556
+ * This gets a temporary authentication token with the device token and then
557
+ * constructs the api instance.
533
558
  *
534
559
  * @param deviceToken - the device token proving this api instance is
535
560
  * registered. Create one with {@link register}.
536
561
  * @returns an api instance
537
562
  */
538
- export declare function remarkable(deviceToken: string, { authHost, rawHost, cache, maxCacheSize, }?: RemarkableOptions): Promise<RemarkableApi>;
563
+ export declare function remarkable(deviceToken: string, options?: 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
  // ------------ //
@@ -125,15 +126,15 @@ export async function register(code, { deviceDesc = "browser-chrome", uuid = uui
125
126
  }
126
127
  /** the implementation of that api */
127
128
  class Remarkable {
128
- #userToken;
129
+ #sessionToken;
129
130
  /** the same cache that underlies the raw api, allowing us to modify it */
130
131
  #cache;
131
132
  raw;
132
133
  #lastHashGen;
133
- constructor(userToken, rawHost, cache) {
134
- this.#userToken = userToken;
134
+ constructor(sessionToken, rawHost, uploadHost, cache) {
135
+ this.#sessionToken = sessionToken;
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) {
@@ -157,10 +158,11 @@ class Remarkable {
157
158
  const resp = await fetch(url, {
158
159
  method,
159
160
  headers: {
160
- Authorization: `Bearer ${this.#userToken}`,
161
+ Authorization: `Bearer ${this.#sessionToken}`,
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) {
@@ -590,16 +596,13 @@ class Remarkable {
590
596
  }
591
597
  const cached = values(nullable(string()));
592
598
  /**
593
- * create an instance of the api
594
- *
595
- * This gets a temporary authentication token with the device token. If
596
- * requests start failing, simply recreate the api instance.
599
+ * Exchange a device token for a session token.
597
600
  *
598
601
  * @param deviceToken - the device token proving this api instance is
599
602
  * registered. Create one with {@link register}.
600
- * @returns an api instance
603
+ * @returns the session token returned by the reMarkable service
601
604
  */
602
- export async function remarkable(deviceToken, { authHost = AUTH_HOST, rawHost = RAW_HOST, cache, maxCacheSize = Infinity, } = {}) {
605
+ export async function auth(deviceToken, { authHost = AUTH_HOST } = {}) {
603
606
  const resp = await fetch(`${authHost}/token/json/2/user/new`, {
604
607
  method: "POST",
605
608
  headers: {
@@ -609,16 +612,46 @@ export async function remarkable(deviceToken, { authHost = AUTH_HOST, rawHost =
609
612
  if (!resp.ok) {
610
613
  throw new Error(`couldn't fetch auth token: ${resp.statusText}`);
611
614
  }
612
- const userToken = await resp.text();
615
+ return await resp.text();
616
+ }
617
+ /**
618
+ * Create an API instance from an existing session token.
619
+ *
620
+ * If requests start failing, simply recreate the api instance with a freshly
621
+ * fetched session token.
622
+ *
623
+ * @param sessionToken - the session token used for authorization
624
+ * @returns an api instance
625
+ */
626
+ export function session(sessionToken, { rawHost = RAW_HOST, uploadHost = UPLOAD_HOST, cache, maxCacheSize = Infinity, } = {}) {
613
627
  const initCache = JSON.parse(cache ?? "{}");
614
628
  if (cached.guard(initCache)) {
615
629
  const entries = Object.entries(initCache);
616
- const cache = maxCacheSize === Infinity
630
+ const cacheMap = maxCacheSize === Infinity
617
631
  ? new Map(entries)
618
632
  : new LruCache(maxCacheSize, entries);
619
- return new Remarkable(userToken, rawHost, cache);
620
- }
621
- else {
622
- 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.");
633
+ return new Remarkable(sessionToken, rawHost, uploadHost, cacheMap);
623
634
  }
635
+ 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.");
636
+ }
637
+ /**
638
+ * create an instance of the api
639
+ *
640
+ * This gets a temporary authentication token with the device token and then
641
+ * constructs the api instance.
642
+ *
643
+ * @param deviceToken - the device token proving this api instance is
644
+ * registered. Create one with {@link register}.
645
+ * @returns an api instance
646
+ */
647
+ export async function remarkable(deviceToken, options = {}) {
648
+ const { authHost, rawHost, uploadHost, cache, maxCacheSize, syncHost } = options ?? {};
649
+ const sessionToken = await auth(deviceToken, { authHost });
650
+ return session(sessionToken, {
651
+ rawHost,
652
+ uploadHost,
653
+ cache,
654
+ maxCacheSize,
655
+ syncHost,
656
+ });
624
657
  }
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
  *
@@ -52,7 +61,7 @@ export interface PageTag extends Tag {
52
61
  /** all supported document orientations */
53
62
  export type Orientation = "portrait" | "landscape";
54
63
  /** all supported text alignments */
55
- export type TextAlignment = "justify" | "left";
64
+ export type TextAlignment = "" | "justify" | "left";
56
65
  /** types of zoom modes for documents, applies primarily to pdf files */
57
66
  export type ZoomMode = "bestFit" | "customFit" | "fitToHeight" | "fitToWidth";
58
67
  /**
@@ -193,8 +202,8 @@ export interface DocumentContent {
193
202
  pageCount: number;
194
203
  /** the page tags for the document */
195
204
  pageTags?: PageTag[];
196
- /** a list of the ids of each page in the document */
197
- pages?: string[];
205
+ /** a list of the ids of each page in the document, or null when never opened */
206
+ pages?: string[] | null;
198
207
  /** a mapping from page number to page id in pages */
199
208
  redirectionPageMap?: number[];
200
209
  /** ostensibly the size in bytes of the file, but this differs from other measurements */
@@ -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
  *
@@ -530,15 +554,13 @@ export interface RawRemarkableApi {
530
554
  /** completely clear the cache */
531
555
  clearCache(): void;
532
556
  }
533
- interface AuthedFetch {
534
- (method: RequestMethod, url: string, init?: {
535
- body?: string | Uint8Array;
536
- headers?: Record<string, string>;
537
- }): Promise<Response>;
538
- }
557
+ type AuthedFetch = (method: RequestMethod, url: string, init?: {
558
+ body?: string | Uint8Array;
559
+ headers?: Record<string, string>;
560
+ }) => Promise<Response>;
539
561
  export declare class RawRemarkable implements RawRemarkableApi {
540
562
  #private;
541
- constructor(authedFetch: AuthedFetch, cache: Map<string, string | null>, rawHost: string);
563
+ constructor(authedFetch: AuthedFetch, cache: Map<string, string | null>, rawHost: string, uploadHost: string);
542
564
  /** make an authorized request to remarkable */
543
565
  getRootHash(): Promise<[string, number]>;
544
566
  getHash(hash: string): Promise<Uint8Array>;
@@ -552,6 +574,7 @@ export declare class RawRemarkable implements RawRemarkableApi {
552
574
  putContent(id: string, content: Content): Promise<[RawFileEntry, Promise<void>]>;
553
575
  putMetadata(id: string, metadata: Metadata): Promise<[RawFileEntry, Promise<void>]>;
554
576
  putEntries(id: string, entries: RawEntry[]): Promise<[RawListEntry, Promise<void>]>;
577
+ uploadFile(visibleName: string, bytes: Uint8Array, mime: UploadMimeType): Promise<SimpleEntry>;
555
578
  dumpCache(): string;
556
579
  clearCache(): void;
557
580
  }
package/dist/raw.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { fromByteArray } from "base64-js";
2
2
  import CRC32C from "crc-32/crc32c";
3
- import { boolean, elements, empty, enumeration, float64, int32, properties, string, timestamp, uint32, uint8, values, } from "jtd-ts";
3
+ import { boolean, elements, empty, enumeration, float64, int32, nullable, properties, string, timestamp, uint8, uint32, values, } from "jtd-ts";
4
4
  import { ValidationError } from "./error.js";
5
5
  const hashReg = /^[0-9a-f]{64}$/;
6
6
  const tag = properties({
@@ -74,7 +74,7 @@ const documentContent = properties({
74
74
  orientation: enumeration("portrait", "landscape"),
75
75
  pageCount: uint32(),
76
76
  sizeInBytes: string(),
77
- textAlignment: enumeration("justify", "left"),
77
+ textAlignment: enumeration("", "justify", "left"),
78
78
  textScale: float64(),
79
79
  }, {
80
80
  cPages,
@@ -90,10 +90,10 @@ const documentContent = properties({
90
90
  count: uint32(),
91
91
  timestamp: float64(),
92
92
  }, undefined, true),
93
- lastOpenedPage: uint32(),
93
+ lastOpenedPage: int32(),
94
94
  margins: uint32(),
95
95
  originalPageCount: int32(),
96
- pages: elements(string()),
96
+ pages: nullable(elements(string())),
97
97
  pageTags: elements(pageTag),
98
98
  redirectionPageMap: elements(int32()),
99
99
  tags: elements(tag),
@@ -134,7 +134,7 @@ const metadata = properties({
134
134
  visibleName: string(),
135
135
  }, {
136
136
  lastOpened: string(),
137
- lastOpenedPage: uint32(),
137
+ lastOpenedPage: int32(),
138
138
  createdTime: string(),
139
139
  deleted: boolean(),
140
140
  metadatamodified: boolean(),
@@ -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() {
@@ -235,7 +243,7 @@ export class RawRemarkable {
235
243
  async getEntries(hash) {
236
244
  const rawFile = await this.getText(hash);
237
245
  const [version, ...rest] = rawFile.slice(0, -1).split("\n");
238
- if (version != "3") {
246
+ if (version !== "3") {
239
247
  throw new Error(`schema version ${version} not supported`);
240
248
  }
241
249
  else {
@@ -253,8 +261,8 @@ export class RawRemarkable {
253
261
  hash,
254
262
  type: 80000000,
255
263
  id,
256
- subfiles: parseInt(subfiles),
257
- size: parseInt(size),
264
+ subfiles: parseInt(subfiles, 10),
265
+ size: parseInt(size, 10),
258
266
  };
259
267
  }
260
268
  else if (type === "0" && subfiles === "0") {
@@ -263,7 +271,7 @@ export class RawRemarkable {
263
271
  type: 0,
264
272
  id,
265
273
  subfiles: 0,
266
- size: parseInt(size),
274
+ size: parseInt(size, 10),
267
275
  };
268
276
  }
269
277
  else {
@@ -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
  }