rmapi-js 8.4.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/README.md +13 -4
- package/dist/index.d.ts +32 -8
- package/dist/index.js +83 -57
- package/dist/raw.d.ts +40 -41
- package/dist/raw.js +85 -59
- package/dist/rmapi-js.esm.min.js +15 -18
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +10 -0
- package/package.json +10 -18
package/dist/raw.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { fromByteArray } from "base64-js";
|
|
2
1
|
import CRC32C from "crc-32/crc32c";
|
|
3
|
-
import { boolean, elements, empty, enumeration, float64, int32, properties, string, timestamp,
|
|
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(),
|
|
@@ -74,7 +75,7 @@ const documentContent = properties({
|
|
|
74
75
|
orientation: enumeration("portrait", "landscape"),
|
|
75
76
|
pageCount: uint32(),
|
|
76
77
|
sizeInBytes: string(),
|
|
77
|
-
textAlignment: enumeration("justify", "left"),
|
|
78
|
+
textAlignment: enumeration("", "justify", "left"),
|
|
78
79
|
textScale: float64(),
|
|
79
80
|
}, {
|
|
80
81
|
cPages,
|
|
@@ -90,10 +91,10 @@ const documentContent = properties({
|
|
|
90
91
|
count: uint32(),
|
|
91
92
|
timestamp: float64(),
|
|
92
93
|
}, undefined, true),
|
|
93
|
-
lastOpenedPage:
|
|
94
|
+
lastOpenedPage: int32(),
|
|
94
95
|
margins: uint32(),
|
|
95
96
|
originalPageCount: int32(),
|
|
96
|
-
pages: elements(string()),
|
|
97
|
+
pages: nullable(elements(string())),
|
|
97
98
|
pageTags: elements(pageTag),
|
|
98
99
|
redirectionPageMap: elements(int32()),
|
|
99
100
|
tags: elements(tag),
|
|
@@ -134,7 +135,7 @@ const metadata = properties({
|
|
|
134
135
|
visibleName: string(),
|
|
135
136
|
}, {
|
|
136
137
|
lastOpened: string(),
|
|
137
|
-
lastOpenedPage:
|
|
138
|
+
lastOpenedPage: int32(),
|
|
138
139
|
createdTime: string(),
|
|
139
140
|
deleted: boolean(),
|
|
140
141
|
metadatamodified: boolean(),
|
|
@@ -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
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
247
|
-
|
|
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
|
-
|
|
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),
|
|
265
|
-
size: parseInt(size),
|
|
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),
|
|
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 =
|
|
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 = [
|
|
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`,
|
|
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 =
|
|
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: {
|