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