rmapi-js 8.5.0 → 9.0.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
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, CPageStringValue, CPages, CPageUUID, DocumentContent, DocumentMetadata, FileType, KeyboardMetadata, Metadata, Orientation, PageTag, RawEntry, RawFileEntry, RawListEntry, RawRemarkableApi, SimpleEntry, Tag, TemplateContent, TextAlignment, UploadMimeType, ZoomMode, } from "./raw";
3
+ export type { BackgroundFilter, CollectionContent, Content, CPageNumberValue, CPagePage, CPageStringValue, CPages, CPageUUID, DocumentContent, DocumentMetadata, Entries, FileType, KeyboardMetadata, Metadata, Orientation, PageTag, RawEntry, RawRemarkableApi, SchemaVersion, 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 */
@@ -226,7 +226,7 @@ export interface RemarkableApi {
226
226
  * @remarks
227
227
  * If this fails validation and you still want to get the content, you can use
228
228
  * the low-level api to get the raw text of the `.content` file in the
229
- * `RawListEntry` for this hash.
229
+ * `RawEntry` for this hash.
230
230
  *
231
231
  * @param hash - the hash of the item to get content for
232
232
  * @returns the content
@@ -241,7 +241,7 @@ export interface RemarkableApi {
241
241
  * @remarks
242
242
  * If this fails validation and you still want to get the content, you can use
243
243
  * the low-level api to get the raw text of the `.metadata` file in the
244
- * `RawListEntry` for this hash.
244
+ * `RawEntry` for this hash.
245
245
  *
246
246
  * @param hash - the hash of the item to get metadata for
247
247
  * @returns the metadata
package/dist/index.js CHANGED
@@ -144,7 +144,9 @@ class Remarkable {
144
144
  }
145
145
  async #putRootHash(hash, generation) {
146
146
  try {
147
- this.#lastHashGen = await this.raw.putRootHash(hash, generation);
147
+ const [rootHash, gen] = await this.raw.putRootHash(hash, generation);
148
+ const [, , schemaVersion] = this.#lastHashGen; // guaranteed to be set
149
+ this.#lastHashGen = [rootHash, gen, schemaVersion];
148
150
  }
149
151
  catch (ex) {
150
152
  // if we hit a generation error, invalidate our cached generation
@@ -178,7 +180,7 @@ class Remarkable {
178
180
  }
179
181
  }
180
182
  async #convertEntry({ hash, id }) {
181
- const entries = await this.raw.getEntries(hash);
183
+ const { entries } = await this.raw.getEntries(hash);
182
184
  const metaEnt = entries.find((ent) => ent.id.endsWith(".metadata"));
183
185
  const contentEnt = entries.find((ent) => ent.id.endsWith(".content"));
184
186
  if (metaEnt === undefined) {
@@ -238,11 +240,11 @@ class Remarkable {
238
240
  }
239
241
  async listIds(refresh = false) {
240
242
  const [hash] = await this.#getRootHash(refresh);
241
- const entries = await this.raw.getEntries(hash);
243
+ const { entries } = await this.raw.getEntries(hash);
242
244
  return entries.map(({ id, hash }) => ({ id, hash }));
243
245
  }
244
246
  async getContent(hash) {
245
- const entries = await this.raw.getEntries(hash);
247
+ const { entries } = await this.raw.getEntries(hash);
246
248
  const [cont] = entries.filter((e) => e.id.endsWith(".content"));
247
249
  if (cont === undefined) {
248
250
  throw new Error(`couldn't find contents for hash ${hash}`);
@@ -252,7 +254,7 @@ class Remarkable {
252
254
  }
253
255
  }
254
256
  async getMetadata(hash) {
255
- const entries = await this.raw.getEntries(hash);
257
+ const { entries } = await this.raw.getEntries(hash);
256
258
  const [meta] = entries.filter((e) => e.id.endsWith(".metadata"));
257
259
  if (meta === undefined) {
258
260
  throw new Error(`couldn't find metadata for hash ${hash}`);
@@ -262,7 +264,7 @@ class Remarkable {
262
264
  }
263
265
  }
264
266
  async getPdf(hash) {
265
- const entries = await this.raw.getEntries(hash);
267
+ const { entries } = await this.raw.getEntries(hash);
266
268
  const [pdf] = entries.filter((e) => e.id.endsWith(".pdf"));
267
269
  if (pdf === undefined) {
268
270
  throw new Error(`couldn't find pdf for hash ${hash}`);
@@ -272,7 +274,7 @@ class Remarkable {
272
274
  }
273
275
  }
274
276
  async getEpub(hash) {
275
- const entries = await this.raw.getEntries(hash);
277
+ const { entries } = await this.raw.getEntries(hash);
276
278
  const [epub] = entries.filter((e) => e.id.endsWith(".epub"));
277
279
  if (epub === undefined) {
278
280
  throw new Error(`couldn't find epub for hash ${hash}`);
@@ -282,7 +284,7 @@ class Remarkable {
282
284
  }
283
285
  }
284
286
  async getDocument(hash) {
285
- const entries = await this.raw.getEntries(hash);
287
+ const { entries } = await this.raw.getEntries(hash);
286
288
  const zip = new JSZip();
287
289
  for (const entry of entries) {
288
290
  // TODO if this is .metadata we might want to assert type === "DocumentType"
@@ -331,7 +333,7 @@ class Remarkable {
331
333
  sizeInBytes: buffer.length.toFixed(),
332
334
  };
333
335
  // upload raw files, and get root hash
334
- const [[contentEntry, uploadContent], [metadataEntry, uploadMetadata], [pagedataEntry, uploadPagedata], [fileEntry, uploadFile], [rootHash, generation],] = await Promise.all([
336
+ const [[contentEntry, uploadContent], [metadataEntry, uploadMetadata], [pagedataEntry, uploadPagedata], [fileEntry, uploadFile], [rootHash, generation, schemaVersion],] = await Promise.all([
335
337
  this.raw.putContent(`${id}.content`, content),
336
338
  this.raw.putMetadata(`${id}.metadata`, metadata),
337
339
  // eslint-disable-next-line spellcheck/spell-checker
@@ -340,18 +342,13 @@ class Remarkable {
340
342
  this.#getRootHash(refresh),
341
343
  ]);
342
344
  // now fetch root entries and upload this file entry
343
- const [[collectionEntry, uploadCollection], rootEntries] = await Promise.all([
344
- this.raw.putEntries(id, [
345
- contentEntry,
346
- metadataEntry,
347
- pagedataEntry,
348
- fileEntry,
349
- ]),
345
+ const [[collectionEntry, uploadCollection], { entries: rootEntries }] = await Promise.all([
346
+ this.raw.putEntries(id, [contentEntry, metadataEntry, pagedataEntry, fileEntry], schemaVersion),
350
347
  this.raw.getEntries(rootHash),
351
348
  ]);
352
349
  // now upload a new root entry
353
350
  rootEntries.push(collectionEntry);
354
- const [rootEntry, uploadRoot] = await this.raw.putEntries("root", rootEntries);
351
+ const [rootEntry, uploadRoot] = await this.raw.putEntries("root", rootEntries, schemaVersion);
355
352
  // before updating the root hash, first upload everything
356
353
  await Promise.all([
357
354
  uploadContent,
@@ -393,19 +390,19 @@ class Remarkable {
393
390
  visibleName,
394
391
  };
395
392
  // upload folder contents
396
- const [[contentEntry, uploadContent], [metadataEntry, uploadMetadata], [rootHash, generation],] = await Promise.all([
393
+ const [[contentEntry, uploadContent], [metadataEntry, uploadMetadata], [rootHash, generation, schemaVersion],] = await Promise.all([
397
394
  this.raw.putContent(`${id}.content`, content),
398
395
  this.raw.putMetadata(`${id}.metadata`, metadata),
399
396
  this.#getRootHash(refresh),
400
397
  ]);
401
398
  // now fetch root entries and upload this file entry
402
- const [[collectionEntry, uploadCollection], rootEntries] = await Promise.all([
403
- this.raw.putEntries(id, [contentEntry, metadataEntry]),
399
+ const [[collectionEntry, uploadCollection], { entries: rootEntries }] = await Promise.all([
400
+ this.raw.putEntries(id, [contentEntry, metadataEntry], schemaVersion),
404
401
  this.raw.getEntries(rootHash),
405
402
  ]);
406
403
  // now upload a new root entry
407
404
  rootEntries.push(collectionEntry);
408
- const [rootEntry, uploadRoot] = await this.raw.putEntries("root", rootEntries);
405
+ const [rootEntry, uploadRoot] = await this.raw.putEntries("root", rootEntries, schemaVersion);
409
406
  // before updating the root hash, first upload everything
410
407
  await Promise.all([
411
408
  uploadContent,
@@ -430,8 +427,8 @@ class Remarkable {
430
427
  return await this.raw.uploadFile(visibleName, new Uint8Array(0), "folder");
431
428
  }
432
429
  /** edit just a content entry */
433
- async #editContentRaw(id, hash, update) {
434
- const entries = await this.raw.getEntries(hash);
430
+ async #editContentRaw(id, hash, update, schemaVersion) {
431
+ const { entries } = await this.raw.getEntries(hash);
435
432
  const contInd = entries.findIndex((ent) => ent.id.endsWith(".content"));
436
433
  const contEntry = entries[contInd];
437
434
  if (contEntry === undefined) {
@@ -441,28 +438,28 @@ class Remarkable {
441
438
  Object.assign(cont, update);
442
439
  const [newContEntry, uploadCont] = await this.raw.putContent(contEntry.id, cont);
443
440
  entries[contInd] = newContEntry;
444
- const [result, uploadEntries] = await this.raw.putEntries(id, entries);
441
+ const [result, uploadEntries] = await this.raw.putEntries(id, entries, schemaVersion);
445
442
  const upload = Promise.all([uploadCont, uploadEntries]);
446
443
  return [result, upload];
447
444
  }
448
445
  /** fully sync a content edit */
449
446
  async #editContent(hash, update, expectedType, refresh) {
450
- const [rootHash, generation] = await this.#getRootHash(refresh);
451
- const entries = await this.raw.getEntries(rootHash);
447
+ const [rootHash, generation, schemaVersion] = await this.#getRootHash(refresh);
448
+ const { entries } = await this.raw.getEntries(rootHash);
452
449
  const hashInd = entries.findIndex((ent) => ent.hash === hash);
453
450
  const hashEnt = entries[hashInd];
454
451
  if (hashEnt === undefined) {
455
452
  throw new HashNotFoundError(hash);
456
453
  }
457
454
  const [[newEnt, uploadEnt], meta] = await Promise.all([
458
- this.#editContentRaw(hashEnt.id, hash, update),
455
+ this.#editContentRaw(hashEnt.id, hash, update, schemaVersion),
459
456
  this.getMetadata(hash),
460
457
  ]);
461
458
  if (meta.type !== expectedType) {
462
459
  throw new Error(`expected type ${expectedType} but got ${meta.type} for hash ${hash}`);
463
460
  }
464
461
  entries[hashInd] = newEnt;
465
- const [rootEntry, uploadRoot] = await this.raw.putEntries("root", entries);
462
+ const [rootEntry, uploadRoot] = await this.raw.putEntries("root", entries, schemaVersion);
466
463
  await Promise.all([uploadEnt, uploadRoot]);
467
464
  await this.#putRootHash(rootEntry.hash, generation);
468
465
  return { hash: newEnt.hash };
@@ -479,8 +476,8 @@ class Remarkable {
479
476
  async updateTemplate(hash, content, refresh = false) {
480
477
  return await this.#editContent(hash, content, "TemplateType", refresh);
481
478
  }
482
- async #editMetaRaw(id, hash, update) {
483
- const entries = await this.raw.getEntries(hash);
479
+ async #editMetaRaw(id, hash, update, schemaVersion) {
480
+ const { entries } = await this.raw.getEntries(hash);
484
481
  const metaInd = entries.findIndex((ent) => ent.id.endsWith(".metadata"));
485
482
  const metaEntry = entries[metaInd];
486
483
  if (metaEntry === undefined) {
@@ -490,21 +487,21 @@ class Remarkable {
490
487
  Object.assign(meta, update);
491
488
  const [newMetaEntry, uploadMeta] = await this.raw.putMetadata(metaEntry.id, meta);
492
489
  entries[metaInd] = newMetaEntry;
493
- const [result, uploadEntries] = await this.raw.putEntries(id, entries);
490
+ const [result, uploadEntries] = await this.raw.putEntries(id, entries, schemaVersion);
494
491
  const upload = Promise.all([uploadMeta, uploadEntries]);
495
492
  return [result, upload];
496
493
  }
497
494
  async #editMeta(hash, update, refresh = false) {
498
- const [rootHash, generation] = await this.#getRootHash(refresh);
499
- const entries = await this.raw.getEntries(rootHash);
495
+ const [rootHash, generation, schemaVersion] = await this.#getRootHash(refresh);
496
+ const { entries } = await this.raw.getEntries(rootHash);
500
497
  const hashInd = entries.findIndex((ent) => ent.hash === hash);
501
498
  const hashEnt = entries[hashInd];
502
499
  if (hashEnt === undefined) {
503
500
  throw new HashNotFoundError(hash);
504
501
  }
505
- const [newEnt, uploadEnt] = await this.#editMetaRaw(hashEnt.id, hash, update);
502
+ const [newEnt, uploadEnt] = await this.#editMetaRaw(hashEnt.id, hash, update, schemaVersion);
506
503
  entries[hashInd] = newEnt;
507
- const [rootEntry, uploadRoot] = await this.raw.putEntries("root", entries);
504
+ const [rootEntry, uploadRoot] = await this.raw.putEntries("root", entries, schemaVersion);
508
505
  await Promise.all([uploadEnt, uploadRoot]);
509
506
  await this.#putRootHash(rootEntry.hash, generation);
510
507
  return { hash: newEnt.hash };
@@ -533,8 +530,8 @@ class Remarkable {
533
530
  if (!idReg.test(parent)) {
534
531
  throw new ValidationError(parent, idReg, "parent must be a valid document id");
535
532
  }
536
- const [rootHash, generation] = await this.#getRootHash(refresh);
537
- const entries = await this.raw.getEntries(rootHash);
533
+ const [rootHash, generation, schemaVersion] = await this.#getRootHash(refresh);
534
+ const { entries } = await this.raw.getEntries(rootHash);
538
535
  const hashSet = new Set(hashes);
539
536
  const toUpdate = [];
540
537
  const newEntries = [];
@@ -542,7 +539,7 @@ class Remarkable {
542
539
  const part = hashSet.has(entry.hash) ? toUpdate : newEntries;
543
540
  part.push(entry);
544
541
  }
545
- const resolved = await Promise.all(toUpdate.map(({ id, hash }) => this.#editMetaRaw(id, hash, { parent })));
542
+ const resolved = await Promise.all(toUpdate.map(({ id, hash }) => this.#editMetaRaw(id, hash, { parent }, schemaVersion)));
546
543
  const uploads = [];
547
544
  const result = {};
548
545
  for (const [i, [newEnt, upload]] of resolved.entries()) {
@@ -550,7 +547,7 @@ class Remarkable {
550
547
  uploads.push(upload);
551
548
  result[toUpdate[i].hash] = newEnt.hash;
552
549
  }
553
- const [rootEntry, uploadRoot] = await this.raw.putEntries("root", newEntries);
550
+ const [rootEntry, uploadRoot] = await this.raw.putEntries("root", newEntries, schemaVersion);
554
551
  await Promise.all([Promise.all(uploads), uploadRoot]);
555
552
  await this.#putRootHash(rootEntry.hash, generation);
556
553
  return { hashes: result };
@@ -571,7 +568,8 @@ class Remarkable {
571
568
  // should only go one step) to track all hashes encountered
572
569
  // NOTE that we could increase the cache in this process, or it's possible
573
570
  // for other calls to increase the cache with misc values.
574
- let entries = [await this.raw.getEntries(rootHash)];
571
+ const base = await this.raw.getEntries(rootHash);
572
+ let entries = [base.entries];
575
573
  let nextEntries = [];
576
574
  while (entries.length) {
577
575
  for (const entryList of entries) {
@@ -582,7 +580,8 @@ class Remarkable {
582
580
  }
583
581
  }
584
582
  }
585
- entries = await Promise.all(nextEntries);
583
+ const resolved = await Promise.all(nextEntries);
584
+ entries = resolved.map((ent) => ent.entries);
586
585
  nextEntries = [];
587
586
  }
588
587
  for (const key of toDelete) {
package/dist/raw.d.ts CHANGED
@@ -1,7 +1,10 @@
1
+ import "core-js/proposals/array-buffer-base64";
1
2
  /** request types */
2
3
  export type RequestMethod = "POST" | "GET" | "PUT" | "DELETE" | "PATCH" | "OPTIONS";
3
4
  /** the supported upload mime types */
4
5
  export type UploadMimeType = "application/pdf" | "application/epub+zip" | "folder";
6
+ /** the schema version */
7
+ export type SchemaVersion = 3 | 4;
5
8
  /** an simple entry without any extra information */
6
9
  export interface SimpleEntry {
7
10
  /** the document id */
@@ -17,9 +20,9 @@ export interface SimpleEntry {
17
20
  * files, the high level entry will have the same hash and id as the low-level
18
21
  * entry for that collection.
19
22
  */
20
- export interface RawListEntry {
21
- /** collection type (80000000) */
22
- type: 80000000;
23
+ export interface RawEntry {
24
+ /** 80000000 for schema 3 collection type or 0 for schema 4 or schema 3 files or */
25
+ type: 80000000 | 0;
23
26
  /** the hash of the collection this points to */
24
27
  hash: string;
25
28
  /** the unique id of the collection */
@@ -29,23 +32,21 @@ export interface RawListEntry {
29
32
  /** the total size of everything in the collection */
30
33
  size: number;
31
34
  }
32
- /** the low-level entry for a single file */
33
- export interface RawFileEntry {
34
- /** file type (0) */
35
- type: 0;
36
- /** the hash of the file this points to */
37
- hash: string;
38
- /** the unique id of the file */
39
- id: string;
40
- /** the number of subfiles, always zero */
41
- subfiles: 0;
42
- /** the size of the file in bytes */
43
- size: number;
44
- }
45
- /** a low-level stored entry */
46
- export type RawEntry = RawListEntry | RawFileEntry;
47
35
  /** the type of files reMarkable supports */
48
36
  export type FileType = "epub" | "pdf" | "notebook";
37
+ /**
38
+ * a parsed entries file
39
+ *
40
+ * id and size are defined for schema 4 but not for 3
41
+ */
42
+ export interface Entries {
43
+ /** the raw entries in the file */
44
+ entries: RawEntry[];
45
+ /** the id of this entry, only specified for schema 4 */
46
+ id?: string;
47
+ /** the recursive size of this entry, only specified for schema 4 */
48
+ size?: number;
49
+ }
49
50
  /** a tag for an entry */
50
51
  export interface Tag {
51
52
  /** the name of the tag */
@@ -426,7 +427,7 @@ export interface RawRemarkableApi {
426
427
  *
427
428
  * @returns the root hash and the current generation
428
429
  */
429
- getRootHash(): Promise<[string, number]>;
430
+ getRootHash(): Promise<[string, number, SchemaVersion]>;
430
431
  /**
431
432
  * get the raw binary data associated with a hash
432
433
  *
@@ -453,7 +454,7 @@ export interface RawRemarkableApi {
453
454
  * @param hash - the hash to get entries for
454
455
  * @returns the entries
455
456
  */
456
- getEntries(hash: string): Promise<RawEntry[]>;
457
+ getEntries(hash: string): Promise<Entries>;
457
458
  /**
458
459
  * get the parsed and validated `Content` of a content hash
459
460
  *
@@ -508,13 +509,13 @@ export interface RawRemarkableApi {
508
509
  * @param bytes - the bytes to upload
509
510
  * @returns the new entry and a promise to finish the upload
510
511
  */
511
- putFile(id: string, bytes: Uint8Array): Promise<[RawFileEntry, Promise<void>]>;
512
+ putFile(id: string, bytes: Uint8Array): Promise<[RawEntry, Promise<void>]>;
512
513
  /** the same as {@link putFile | `putFile`} but with caching for text */
513
- putText(id: string, content: string): Promise<[RawFileEntry, Promise<void>]>;
514
+ putText(id: string, content: string): Promise<[RawEntry, Promise<void>]>;
514
515
  /** the same as {@link putText | `putText`} but with extra validation for Content */
515
- putContent(id: string, content: Content): Promise<[RawFileEntry, Promise<void>]>;
516
+ putContent(id: string, content: Content): Promise<[RawEntry, Promise<void>]>;
516
517
  /** the same as {@link putText | `putText`} but with extra validation for Metadata */
517
- putMetadata(id: string, metadata: Metadata): Promise<[RawFileEntry, Promise<void>]>;
518
+ putMetadata(id: string, metadata: Metadata): Promise<[RawEntry, Promise<void>]>;
518
519
  /**
519
520
  * put a set of entries to make an entry list file
520
521
  *
@@ -530,7 +531,7 @@ export interface RawRemarkableApi {
530
531
  *
531
532
  * @returns the new list entry and a promise to finish the upload
532
533
  */
533
- putEntries(id: string, entries: RawEntry[]): Promise<[RawListEntry, Promise<void>]>;
534
+ putEntries(id: string, entries: RawEntry[], schemaVersion: SchemaVersion): Promise<[RawEntry, Promise<void>]>;
534
535
  /**
535
536
  * upload a file to the reMarkable cloud using the simple api
536
537
  *
@@ -562,18 +563,18 @@ export declare class RawRemarkable implements RawRemarkableApi {
562
563
  #private;
563
564
  constructor(authedFetch: AuthedFetch, cache: Map<string, string | null>, rawHost: string, uploadHost: string);
564
565
  /** make an authorized request to remarkable */
565
- getRootHash(): Promise<[string, number]>;
566
+ getRootHash(): Promise<[string, number, SchemaVersion]>;
566
567
  getHash(hash: string): Promise<Uint8Array>;
567
568
  getText(hash: string): Promise<string>;
568
- getEntries(hash: string): Promise<RawEntry[]>;
569
+ getEntries(hash: string): Promise<Entries>;
569
570
  getContent(hash: string): Promise<Content>;
570
571
  getMetadata(hash: string): Promise<Metadata>;
571
572
  putRootHash(hash: string, generation: number, broadcast?: boolean): Promise<[string, number]>;
572
- putFile(id: string, bytes: Uint8Array): Promise<[RawFileEntry, Promise<void>]>;
573
- putText(id: string, text: string): Promise<[RawFileEntry, Promise<void>]>;
574
- putContent(id: string, content: Content): Promise<[RawFileEntry, Promise<void>]>;
575
- putMetadata(id: string, metadata: Metadata): Promise<[RawFileEntry, Promise<void>]>;
576
- putEntries(id: string, entries: RawEntry[]): Promise<[RawListEntry, Promise<void>]>;
573
+ putFile(id: string, bytes: Uint8Array): Promise<[RawEntry, Promise<void>]>;
574
+ putText(id: string, text: string): Promise<[RawEntry, Promise<void>]>;
575
+ putContent(id: string, content: Content): Promise<[RawEntry, Promise<void>]>;
576
+ putMetadata(id: string, metadata: Metadata): Promise<[RawEntry, Promise<void>]>;
577
+ putEntries(id: string, entries: RawEntry[], schemaVersion: SchemaVersion): Promise<[RawEntry, Promise<void>]>;
577
578
  uploadFile(visibleName: string, bytes: Uint8Array, mime: UploadMimeType): Promise<SimpleEntry>;
578
579
  dumpCache(): string;
579
580
  clearCache(): void;
package/dist/raw.js CHANGED
@@ -1,7 +1,8 @@
1
- import { fromByteArray } from "base64-js";
2
1
  import CRC32C from "crc-32/crc32c";
3
2
  import { boolean, elements, empty, enumeration, float64, int32, nullable, properties, string, timestamp, uint8, uint32, values, } from "jtd-ts";
4
3
  import { ValidationError } from "./error.js";
4
+ import { concatArrays } from "./utils.js";
5
+ import "core-js/proposals/array-buffer-base64";
5
6
  const hashReg = /^[0-9a-f]{64}$/;
6
7
  const tag = properties({
7
8
  name: string(),
@@ -159,9 +160,29 @@ async function digest(buff) {
159
160
  const digest = await crypto.subtle.digest("SHA-256",
160
161
  // NOTE this is type hinted wrong, but it does work correctly on a uint8 view
161
162
  buff);
162
- return [...new Uint8Array(digest)]
163
- .map((x) => x.toString(16).padStart(2, "0"))
164
- .join("");
163
+ return new Uint8Array(digest).toHex();
164
+ }
165
+ function parseRawEntryLine(line) {
166
+ const [hash, type, id, subfiles, size] = line.split(":");
167
+ if (hash === undefined ||
168
+ type === undefined ||
169
+ id === undefined ||
170
+ subfiles === undefined ||
171
+ size === undefined) {
172
+ throw new Error(`line '${line}' was not formatted correctly`);
173
+ }
174
+ else if (type === "80000000" || type === "0") {
175
+ return {
176
+ hash,
177
+ type: type === "0" ? 0 : 80000000,
178
+ id,
179
+ subfiles: parseInt(subfiles, 10),
180
+ size: parseInt(size, 10),
181
+ };
182
+ }
183
+ else {
184
+ throw new Error(`line '${line}' was not formatted correctly`);
185
+ }
165
186
  }
166
187
  export class RawRemarkable {
167
188
  #authedFetch;
@@ -191,14 +212,14 @@ export class RawRemarkable {
191
212
  if (!rootHash.guardAssert(loaded))
192
213
  throw Error("invalid root hash");
193
214
  const { hash, generation, schemaVersion } = loaded;
194
- if (schemaVersion !== 3) {
215
+ if (schemaVersion !== 3 && schemaVersion !== 4) {
195
216
  throw new Error(`schema version ${schemaVersion} not supported`);
196
217
  }
197
218
  else if (!Number.isSafeInteger(generation)) {
198
219
  throw new Error(`generation ${generation} was not a safe integer; please file a bug report`);
199
220
  }
200
221
  else {
201
- return [hash, generation];
222
+ return [hash, generation, schemaVersion];
202
223
  }
203
224
  }
204
225
  async #getHash(hash) {
@@ -243,41 +264,30 @@ export class RawRemarkable {
243
264
  async getEntries(hash) {
244
265
  const rawFile = await this.getText(hash);
245
266
  const [version, ...rest] = rawFile.slice(0, -1).split("\n");
246
- if (version !== "3") {
247
- throw new Error(`schema version ${version} not supported`);
267
+ if (version === "3") {
268
+ return { entries: rest.map(parseRawEntryLine) };
269
+ }
270
+ else if (version === "4") {
271
+ const [info, ...remaining] = rest;
272
+ if (!info)
273
+ throw new Error("missing info line for schema version 4");
274
+ const [lead, id, count, size] = info.split(":");
275
+ if (lead !== "0" ||
276
+ id === undefined ||
277
+ count === undefined ||
278
+ size === undefined) {
279
+ throw new Error(`schema 4 info line '${info}' was not formatted correctly`);
280
+ }
281
+ const entries = remaining.map(parseRawEntryLine);
282
+ if (parseInt(count, 10) !== entries.length) {
283
+ throw new Error(`schema 4 expected ${count} entries, but found ${entries.length}`);
284
+ }
285
+ else {
286
+ return { entries, id, size: parseInt(size, 10) };
287
+ }
248
288
  }
249
289
  else {
250
- return rest.map((line) => {
251
- const [hash, type, id, subfiles, size] = line.split(":");
252
- if (hash === undefined ||
253
- type === undefined ||
254
- id === undefined ||
255
- subfiles === undefined ||
256
- size === undefined) {
257
- throw new Error(`line '${line}' was not formatted correctly`);
258
- }
259
- else if (type === "80000000") {
260
- return {
261
- hash,
262
- type: 80000000,
263
- id,
264
- subfiles: parseInt(subfiles, 10),
265
- size: parseInt(size, 10),
266
- };
267
- }
268
- else if (type === "0" && subfiles === "0") {
269
- return {
270
- hash,
271
- type: 0,
272
- id,
273
- subfiles: 0,
274
- size: parseInt(size, 10),
275
- };
276
- }
277
- else {
278
- throw new Error(`line '${line}' was not formatted correctly`);
279
- }
280
- });
290
+ throw new Error(`schema version ${version} not supported`);
281
291
  }
282
292
  }
283
293
  async getContent(hash) {
@@ -341,7 +351,7 @@ export class RawRemarkable {
341
351
  const crc = CRC32C.buf(bytes, 0);
342
352
  const buff = new ArrayBuffer(4);
343
353
  new DataView(buff).setInt32(0, crc, false);
344
- const crcHash = fromByteArray(new Uint8Array(buff));
354
+ const crcHash = new Uint8Array(buff).toBase64();
345
355
  await this.#authedFetch("PUT", `${this.#rawHost}/sync/v3/files/${hash}`, {
346
356
  body: bytes,
347
357
  headers: {
@@ -396,39 +406,55 @@ export class RawRemarkable {
396
406
  return await this.putText(id, JSON.stringify(metadata));
397
407
  }
398
408
  }
399
- async putEntries(id, entries) {
400
- // NOTE collections have a special hash function, the hash of their
409
+ async putEntries(id, entries, schemaVersion) {
410
+ // NOTE v3 collections have a special hash function, the hash of their
401
411
  // contents, so this needs to be different
402
412
  entries.sort((a, b) => a.id.localeCompare(b.id));
403
- const hashBuff = new Uint8Array(entries.length * 32);
404
- for (const [start, { hash }] of entries.entries()) {
405
- for (const [i, byte] of (hash.match(/../g) ?? []).entries()) {
406
- hashBuff[start * 32 + i] = parseInt(byte, 16);
407
- }
408
- }
409
- const hash = await digest(hashBuff);
410
413
  const size = entries.reduce((acc, ent) => acc + ent.size, 0);
411
- const records = ["3\n"];
414
+ const records = [`${schemaVersion}\n`];
415
+ if (schemaVersion === 4) {
416
+ const name = id === "root" ? "." : id;
417
+ records.push(`0:${name}:${entries.length}:${size}\n`);
418
+ }
412
419
  for (const { hash, type, id, subfiles, size } of entries) {
413
420
  records.push(`${hash}:${type}:${id}:${subfiles}:${size}\n`);
414
421
  }
422
+ const enc = new TextEncoder();
423
+ const entryBuff = enc.encode(records.join(""));
424
+ let hash;
425
+ if (schemaVersion === 3) {
426
+ // in schema version 3 an entry's hash is the hash of the concatenated hashes
427
+ const hashBuffs = [];
428
+ for (const { hash } of entries) {
429
+ hashBuffs.push(Uint8Array.fromHex(hash));
430
+ }
431
+ hash = await digest(concatArrays(hashBuffs));
432
+ }
433
+ else if (schemaVersion === 4) {
434
+ // in schema version 4 an entry's hash is the hash of the full entry file, same as everything else
435
+ hash = await digest(entryBuff);
436
+ }
437
+ else {
438
+ throw new Error(`unsupported schema version ${schemaVersion}`);
439
+ }
415
440
  const res = {
416
441
  id,
417
442
  hash,
418
- type: 80000000,
443
+ type: schemaVersion > 3 ? 0 : 80000000,
419
444
  subfiles: entries.length,
420
445
  size,
421
446
  };
422
- const enc = new TextEncoder();
423
447
  return [
424
448
  res,
425
449
  // NOTE when monitoring requests, this had the extension .docSchema appended, but I'm not entirely sure why
426
- this.#putFile(hash, `${id}.docSchema`, enc.encode(records.join(""))),
450
+ this.#putFile(hash, `${id}.docSchema`, entryBuff),
427
451
  ];
428
452
  }
429
453
  async uploadFile(visibleName, bytes, mime) {
430
454
  const enc = new TextEncoder();
431
- const meta = fromByteArray(enc.encode(JSON.stringify({ file_name: visibleName })));
455
+ const meta = enc
456
+ .encode(JSON.stringify({ file_name: visibleName }))
457
+ .toBase64();
432
458
  const resp = await this.#authedFetch("POST", `${this.#uploadHost}/doc/v2/files`, {
433
459
  body: bytes,
434
460
  headers: {