rmapi-js 10.1.1 → 11.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/raw.js CHANGED
@@ -1,172 +1,207 @@
1
1
  import CRC32C from "crc-32/crc32c";
2
- import { boolean, elements, empty, enumeration, float64, int32, nullable, properties, string, timestamp, uint8, uint32, values, } from "jtd-ts";
2
+ import { z } from "zod";
3
3
  import { ValidationError } from "./error.js";
4
4
  import { concatArrays } from "./utils.js";
5
- import "core-js/proposals/array-buffer-base64";
6
5
  const hashReg = /^[0-9a-f]{64}$/;
7
- const tag = properties({
8
- name: string(),
9
- timestamp: float64(),
10
- }, undefined, true);
11
- const pageTag = properties({
12
- name: string(),
13
- pageId: string(),
14
- timestamp: float64(),
15
- }, undefined, true);
16
- const documentMetadata = properties(undefined, {
17
- authors: elements(string()),
18
- title: string(),
19
- publicationDate: string(),
20
- publisher: string(),
21
- }, true);
22
- const cPagePage = properties({
23
- id: string(),
24
- idx: properties({
25
- timestamp: string(),
26
- value: string(),
27
- }, undefined, true),
28
- }, {
29
- template: properties({
30
- timestamp: string(),
31
- value: string(),
32
- }, undefined, true),
33
- redir: properties({
34
- timestamp: string(),
35
- value: int32(),
36
- }, undefined, true),
37
- scrollTime: properties({
38
- timestamp: string(),
39
- value: timestamp(),
40
- }, undefined, true),
41
- verticalScroll: properties({
42
- timestamp: string(),
43
- value: float64(),
44
- }, undefined, true),
45
- deleted: properties({
46
- timestamp: string(),
47
- value: int32(),
48
- }, undefined, true),
49
- }, true);
50
- const cPages = properties({
51
- lastOpened: properties({
52
- timestamp: string(),
53
- value: string(),
54
- }, undefined, true),
55
- original: properties({
56
- timestamp: string(),
57
- value: int32(),
58
- }, undefined, true),
59
- pages: elements(cPagePage),
60
- uuids: nullable(elements(properties({
61
- first: string(),
62
- second: uint32(),
63
- }, undefined, true))),
64
- }, undefined, true);
65
- const collectionContent = properties(undefined, {
66
- tags: elements(tag),
67
- });
68
- const legacyCollectionContent = properties(undefined, {
69
- tags: elements(string()),
70
- });
6
+ const tag = z
7
+ .object({
8
+ name: z.string(),
9
+ timestamp: z.number(),
10
+ })
11
+ .passthrough();
12
+ const pageTag = z
13
+ .object({
14
+ name: z.string(),
15
+ pageId: z.string(),
16
+ timestamp: z.number(),
17
+ })
18
+ .passthrough();
19
+ const documentMetadata = z
20
+ .object({
21
+ authors: z.array(z.string()).optional(),
22
+ title: z.string().optional(),
23
+ publicationDate: z.string().optional(),
24
+ publisher: z.string().optional(),
25
+ })
26
+ .passthrough();
27
+ const cPagePage = z
28
+ .object({
29
+ id: z.string(),
30
+ idx: z.object({ timestamp: z.string(), value: z.string() }).passthrough(),
31
+ template: z
32
+ .object({ timestamp: z.string(), value: z.string() })
33
+ .passthrough()
34
+ .optional(),
35
+ redir: z
36
+ .object({ timestamp: z.string(), value: z.number().int() })
37
+ .passthrough()
38
+ .optional(),
39
+ scrollTime: z
40
+ .object({
41
+ timestamp: z.string(),
42
+ value: z.string().datetime({ offset: true }),
43
+ })
44
+ .passthrough()
45
+ .optional(),
46
+ verticalScroll: z
47
+ .object({ timestamp: z.string(), value: z.number() })
48
+ .passthrough()
49
+ .optional(),
50
+ deleted: z
51
+ .object({ timestamp: z.string(), value: z.number().int() })
52
+ .passthrough()
53
+ .optional(),
54
+ })
55
+ .passthrough();
56
+ const cPages = z
57
+ .object({
58
+ lastOpened: z
59
+ .object({ timestamp: z.string(), value: z.string() })
60
+ .passthrough(),
61
+ original: z
62
+ .object({ timestamp: z.string(), value: z.number().int() })
63
+ .passthrough(),
64
+ pages: z.array(cPagePage),
65
+ uuids: z
66
+ .array(z
67
+ .object({ first: z.string(), second: z.number().int().nonnegative() })
68
+ .passthrough())
69
+ .nullable(),
70
+ })
71
+ .passthrough();
72
+ const collectionContent = z
73
+ .object({
74
+ tags: z.array(tag).optional(),
75
+ })
76
+ .strict();
77
+ const legacyCollectionContent = z
78
+ .object({
79
+ tags: z.array(z.string()).optional(),
80
+ })
81
+ .strict();
71
82
  const documentContentRequired = {
72
- coverPageNumber: int32(),
83
+ coverPageNumber: z.number().int(),
73
84
  documentMetadata,
74
- extraMetadata: values(string()),
75
- fileType: enumeration("epub", "notebook", "pdf"),
76
- fontName: string(),
77
- lineHeight: int32(),
78
- orientation: enumeration("portrait", "landscape"),
79
- pageCount: uint32(),
80
- textAlignment: enumeration("", "justify", "left"),
81
- textScale: float64(),
85
+ extraMetadata: z.record(z.string(), z.string()),
86
+ fileType: z.enum(["epub", "notebook", "pdf"]),
87
+ fontName: z.string(),
88
+ lineHeight: z.number().int(),
89
+ orientation: z.enum(["portrait", "landscape"]),
90
+ pageCount: z.number().int().nonnegative(),
91
+ textAlignment: z.enum(["", "justify", "left"]),
92
+ textScale: z.number(),
82
93
  };
83
94
  const documentContentOptional = {
84
- cPages,
85
- customZoomCenterX: float64(),
86
- customZoomCenterY: float64(),
87
- customZoomOrientation: enumeration("portrait", "landscape"),
88
- customZoomPageHeight: float64(),
89
- customZoomPageWidth: float64(),
90
- customZoomScale: float64(),
91
- dummyDocument: boolean(),
92
- formatVersion: uint8(),
93
- keyboardMetadata: properties({
94
- count: uint32(),
95
- timestamp: float64(),
96
- }, undefined, true),
97
- lastOpenedPage: int32(),
98
- margins: uint32(),
99
- originalPageCount: int32(),
100
- pages: nullable(elements(string())),
101
- pageTags: elements(pageTag),
102
- redirectionPageMap: elements(int32()),
103
- sizeInBytes: string(),
104
- transform: properties(undefined, {
105
- m11: float64(),
106
- m12: float64(),
107
- m13: float64(),
108
- m21: float64(),
109
- m22: float64(),
110
- m23: float64(),
111
- m31: float64(),
112
- m32: float64(),
113
- m33: float64(),
114
- }, true),
115
- // eslint-disable-next-line spellcheck/spell-checker
116
- viewBackgroundFilter: enumeration("off", "fullpage"),
117
- zoomMode: enumeration("bestFit", "customFit", "fitToHeight", "fitToWidth"),
95
+ cPages: cPages.optional(),
96
+ customZoomCenterX: z.number().optional(),
97
+ customZoomCenterY: z.number().optional(),
98
+ customZoomOrientation: z.enum(["portrait", "landscape"]).optional(),
99
+ customZoomPageHeight: z.number().optional(),
100
+ customZoomPageWidth: z.number().optional(),
101
+ customZoomScale: z.number().optional(),
102
+ dummyDocument: z.boolean().optional(),
103
+ formatVersion: z.number().int().nonnegative().optional(),
104
+ keyboardMetadata: z
105
+ .object({ count: z.number().int().nonnegative(), timestamp: z.number() })
106
+ .passthrough()
107
+ .optional(),
108
+ lastOpenedPage: z.number().int().optional(),
109
+ margins: z.number().int().nonnegative().optional(),
110
+ originalPageCount: z.number().int().optional(),
111
+ pages: z.array(z.string()).nullable().optional(),
112
+ pageTags: z.array(pageTag).optional(),
113
+ redirectionPageMap: z.array(z.number().int()).optional(),
114
+ sizeInBytes: z.string().optional(),
115
+ transform: z
116
+ .object({
117
+ m11: z.number().optional(),
118
+ m12: z.number().optional(),
119
+ m13: z.number().optional(),
120
+ m21: z.number().optional(),
121
+ m22: z.number().optional(),
122
+ m23: z.number().optional(),
123
+ m31: z.number().optional(),
124
+ m32: z.number().optional(),
125
+ m33: z.number().optional(),
126
+ })
127
+ .passthrough()
128
+ .optional(),
129
+ viewBackgroundFilter: z.enum(["off", "fullpage"]).optional(),
130
+ zoomMode: z
131
+ .enum(["bestFit", "customFit", "fitToHeight", "fitToWidth"])
132
+ .optional(),
118
133
  };
119
- const documentContent = properties(documentContentRequired, {
120
- ...documentContentOptional,
121
- tags: elements(tag),
122
- }, true);
123
- const legacyDocumentContent = properties(documentContentRequired, {
124
- ...documentContentOptional,
125
- tags: elements(string()),
126
- }, true);
127
- const templateContent = properties({
128
- name: string(),
129
- author: string(),
130
- iconData: string(),
131
- categories: elements(string()),
132
- labels: elements(string()),
133
- orientation: enumeration("portrait", "landscape"),
134
- templateVersion: string(),
135
- supportedScreens: elements(enumeration("rm2", "rmPP")),
136
- constants: elements(values(int32())),
137
- items: elements(empty()),
138
- }, {
139
- formatVersion: uint8(),
140
- });
141
- const metadata = properties({
142
- lastModified: string(),
143
- parent: string(),
144
- pinned: boolean(),
145
- type: enumeration("DocumentType", "CollectionType", "TemplateType"),
146
- visibleName: string(),
147
- }, {
148
- lastOpened: string(),
149
- lastOpenedPage: int32(),
150
- createdTime: string(),
151
- deleted: boolean(),
152
- metadatamodified: boolean(),
153
- modified: boolean(),
154
- synced: boolean(),
155
- version: uint32(),
156
- }, true);
157
- const updatedRootHash = properties({
158
- hash: string(),
159
- generation: float64(),
160
- }, undefined, true);
161
- const rootHash = properties({
162
- hash: string(),
163
- generation: float64(),
164
- schemaVersion: uint8(),
165
- }, undefined, true);
166
- const NativeSimpleEntry = properties({
167
- docID: string(),
168
- hash: string(),
169
- }, undefined, true);
134
+ const commonDocumentContent = z
135
+ .object({ ...documentContentRequired, ...documentContentOptional })
136
+ .passthrough();
137
+ const documentContent = commonDocumentContent
138
+ .extend({ tags: z.array(tag).optional() })
139
+ .passthrough();
140
+ const legacyDocumentContent = commonDocumentContent
141
+ .extend({ tags: z.array(z.string()).optional() })
142
+ .passthrough();
143
+ const templateContent = z
144
+ .object({
145
+ name: z.string(),
146
+ author: z.string(),
147
+ iconData: z.string(),
148
+ categories: z.array(z.string()),
149
+ labels: z.array(z.string()),
150
+ orientation: z.enum(["portrait", "landscape"]),
151
+ templateVersion: z.string(),
152
+ supportedScreens: z.array(z.enum(["rm2", "rmPP"])),
153
+ constants: z.array(z.record(z.string(), z.number().int())).optional(),
154
+ items: z.array(z.unknown()),
155
+ formatVersion: z.number().int().nonnegative().optional(),
156
+ })
157
+ .strict();
158
+ // content payloads aren't discriminable (legacy/modern differ only by tags
159
+ // element type), so this is an ordered union: the first matching variant wins
160
+ const content = z.union([
161
+ collectionContent,
162
+ legacyCollectionContent,
163
+ templateContent,
164
+ documentContent,
165
+ legacyDocumentContent,
166
+ ]);
167
+ const metadata = z
168
+ .object({
169
+ lastModified: z.string(),
170
+ parent: z.string(),
171
+ pinned: z.boolean(),
172
+ type: z.enum(["DocumentType", "CollectionType", "TemplateType"]),
173
+ visibleName: z.string(),
174
+ lastOpened: z.string().optional(),
175
+ lastOpenedPage: z.number().int().optional(),
176
+ createdTime: z.string().optional(),
177
+ deleted: z.boolean().optional(),
178
+ metadatamodified: z.boolean().optional(),
179
+ modified: z.boolean().optional(),
180
+ synced: z.boolean().optional(),
181
+ version: z.number().int().nonnegative().optional(),
182
+ new: z.boolean().optional(),
183
+ source: z.string().optional(),
184
+ })
185
+ .passthrough();
186
+ const updatedRootHash = z
187
+ .object({
188
+ hash: z.string(),
189
+ generation: z.number(),
190
+ })
191
+ .passthrough();
192
+ const rootHash = z
193
+ .object({
194
+ hash: z.string(),
195
+ generation: z.number(),
196
+ schemaVersion: z.number().int().nonnegative(),
197
+ })
198
+ .passthrough();
199
+ const nativeSimpleEntry = z
200
+ .object({
201
+ docID: z.string(),
202
+ hash: z.string(),
203
+ })
204
+ .passthrough();
170
205
  async function digest(buff) {
171
206
  const digest = await crypto.subtle.digest("SHA-256",
172
207
  // NOTE this is type hinted wrong, but it does work correctly on a uint8 view
@@ -220,9 +255,7 @@ export class RawRemarkable {
220
255
  const res = await this.#authedFetch("GET", `${this.#rawHost}/sync/v4/root`);
221
256
  const raw = await res.text();
222
257
  const loaded = JSON.parse(raw);
223
- if (!rootHash.guardAssert(loaded))
224
- throw Error("invalid root hash");
225
- const { hash, generation, schemaVersion } = loaded;
258
+ const { hash, generation, schemaVersion } = rootHash.parse(loaded);
226
259
  if (schemaVersion !== 3 && schemaVersion !== 4) {
227
260
  throw new Error(`schema version ${schemaVersion} not supported`);
228
261
  }
@@ -304,34 +337,12 @@ export class RawRemarkable {
304
337
  async getContent(fileName, hash) {
305
338
  const raw = await this.getText(fileName, hash);
306
339
  const loaded = JSON.parse(raw);
307
- // jtd can't verify non-discriminated unions, in this case, we have fileType
308
- // defined or not. As a result, we try each, and concatenate the errors at the end
309
- const errors = [];
310
- for (const [name, valid] of [
311
- ["collection", collectionContent],
312
- ["legacy collection", legacyCollectionContent],
313
- ["template", templateContent],
314
- ["document", documentContent],
315
- ["legacy document", legacyDocumentContent],
316
- ]) {
317
- try {
318
- if (valid.guardAssert(loaded))
319
- return loaded;
320
- }
321
- catch (ex) {
322
- const msg = ex instanceof Error ? ex.message : "unknown error type";
323
- errors.push(`Couldn't validate as ${name} because:\n${msg}`);
324
- }
325
- }
326
- const joined = errors.join("\n\nor\n\n");
327
- throw new Error(`invalid content: ${joined}`);
340
+ return content.parse(loaded);
328
341
  }
329
342
  async getMetadata(fileName, hash) {
330
343
  const raw = await this.getText(fileName, hash);
331
344
  const loaded = JSON.parse(raw);
332
- if (!metadata.guardAssert(loaded))
333
- throw Error("invalid metadata");
334
- return loaded;
345
+ return metadata.parse(loaded);
335
346
  }
336
347
  async putRootHash(hash, generation, broadcast = true) {
337
348
  if (!Number.isSafeInteger(generation)) {
@@ -348,9 +359,7 @@ export class RawRemarkable {
348
359
  const resp = await this.#authedFetch("PUT", `${this.#rawHost}/sync/v3/root`, { body });
349
360
  const raw = await resp.text();
350
361
  const loaded = JSON.parse(raw);
351
- if (!updatedRootHash.guardAssert(loaded))
352
- throw Error("invalid root hash");
353
- const { hash: newHash, generation: newGen } = loaded;
362
+ const { hash: newHash, generation: newGen } = updatedRootHash.parse(loaded);
354
363
  if (Number.isSafeInteger(newGen)) {
355
364
  return [newHash, newGen];
356
365
  }
@@ -369,7 +378,6 @@ export class RawRemarkable {
369
378
  body: bytes,
370
379
  headers: {
371
380
  "rm-filename": fileName,
372
- // eslint-disable-next-line spellcheck/spell-checker
373
381
  "x-goog-hash": `crc32c=${crcHash}`,
374
382
  },
375
383
  });
@@ -425,14 +433,14 @@ export class RawRemarkable {
425
433
  }
426
434
  // NOTE v3 collections have a special hash function, the hash of their
427
435
  // contents, so this needs to be different
428
- entries.sort((a, b) => a.id.localeCompare(b.id));
429
- const size = entries.reduce((acc, ent) => acc + ent.size, 0);
436
+ const sorted = [...entries].sort((a, b) => a.id.localeCompare(b.id));
437
+ const size = sorted.reduce((acc, ent) => acc + ent.size, 0);
430
438
  const records = [`${schemaVersion}\n`];
431
439
  if (schemaVersion === 4) {
432
440
  const name = id === "root" ? "." : id;
433
- records.push(`0:${name}:${entries.length}:${size}\n`);
441
+ records.push(`0:${name}:${sorted.length}:${size}\n`);
434
442
  }
435
- for (const { hash, type, id, subfiles, size } of entries) {
443
+ for (const { hash, type, id, subfiles, size } of sorted) {
436
444
  const lineType = schemaVersion === 4 ? 0 : type;
437
445
  records.push(`${hash}:${lineType}:${id}:${subfiles}:${size}\n`);
438
446
  }
@@ -442,7 +450,7 @@ export class RawRemarkable {
442
450
  if (schemaVersion === 3) {
443
451
  // in schema version 3 an entry's hash is the hash of the concatenated hashes
444
452
  const hashBuffs = [];
445
- for (const { hash } of entries) {
453
+ for (const { hash } of sorted) {
446
454
  hashBuffs.push(Uint8Array.fromHex(hash));
447
455
  }
448
456
  hash = await digest(concatArrays(hashBuffs));
@@ -458,7 +466,7 @@ export class RawRemarkable {
458
466
  id,
459
467
  hash,
460
468
  type: schemaVersion > 3 ? 0 : 80000000,
461
- subfiles: entries.length,
469
+ subfiles: sorted.length,
462
470
  size,
463
471
  };
464
472
  return [res, this.#putFile(`${id}.docSchema`, hash, entryBuff)];
@@ -477,9 +485,7 @@ export class RawRemarkable {
477
485
  },
478
486
  });
479
487
  const loaded = (await resp.json());
480
- if (!NativeSimpleEntry.guardAssert(loaded))
481
- throw Error("invalid upload response");
482
- const { docID, hash } = loaded;
488
+ const { docID, hash } = nativeSimpleEntry.parse(loaded);
483
489
  return { id: docID, hash };
484
490
  }
485
491
  dumpCache() {