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/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 `
|
|
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.
|
|
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
|
|
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
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,
|
|
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
|
-
* `
|
|
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
|
-
* `
|
|
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
|
|
@@ -482,14 +482,17 @@ export interface RemarkableApi {
|
|
|
482
482
|
*/
|
|
483
483
|
clearCache(): void;
|
|
484
484
|
}
|
|
485
|
-
/**
|
|
486
|
-
export interface
|
|
485
|
+
/** configuration for exchanging a device token */
|
|
486
|
+
export interface AuthOptions {
|
|
487
487
|
/**
|
|
488
488
|
* the url for making authorization requests
|
|
489
489
|
*
|
|
490
490
|
* @defaultValue "https://webapp-prod.cloud.remarkable.engineering"
|
|
491
491
|
*/
|
|
492
492
|
authHost?: string;
|
|
493
|
+
}
|
|
494
|
+
/** options for constructing an api instance from a session token */
|
|
495
|
+
export interface RemarkableSessionOptions {
|
|
493
496
|
/**
|
|
494
497
|
* the url for making synchronization requests
|
|
495
498
|
*
|
|
@@ -526,14 +529,35 @@ export interface RemarkableOptions {
|
|
|
526
529
|
*/
|
|
527
530
|
maxCacheSize?: number;
|
|
528
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;
|
|
529
553
|
/**
|
|
530
554
|
* create an instance of the api
|
|
531
555
|
*
|
|
532
|
-
* This gets a temporary authentication token with the device token
|
|
533
|
-
*
|
|
556
|
+
* This gets a temporary authentication token with the device token and then
|
|
557
|
+
* constructs the api instance.
|
|
534
558
|
*
|
|
535
559
|
* @param deviceToken - the device token proving this api instance is
|
|
536
560
|
* registered. Create one with {@link register}.
|
|
537
561
|
* @returns an api instance
|
|
538
562
|
*/
|
|
539
|
-
export declare function remarkable(deviceToken: string,
|
|
563
|
+
export declare function remarkable(deviceToken: string, options?: RemarkableOptions): Promise<RemarkableApi>;
|
package/dist/index.js
CHANGED
|
@@ -126,13 +126,13 @@ export async function register(code, { deviceDesc = "browser-chrome", uuid = uui
|
|
|
126
126
|
}
|
|
127
127
|
/** the implementation of that api */
|
|
128
128
|
class Remarkable {
|
|
129
|
-
#
|
|
129
|
+
#sessionToken;
|
|
130
130
|
/** the same cache that underlies the raw api, allowing us to modify it */
|
|
131
131
|
#cache;
|
|
132
132
|
raw;
|
|
133
133
|
#lastHashGen;
|
|
134
|
-
constructor(
|
|
135
|
-
this.#
|
|
134
|
+
constructor(sessionToken, rawHost, uploadHost, cache) {
|
|
135
|
+
this.#sessionToken = sessionToken;
|
|
136
136
|
this.#cache = cache;
|
|
137
137
|
this.raw = new RawRemarkable((method, url, { body, headers } = {}) => this.#authedFetch(url, { method, body, headers }), cache, rawHost, uploadHost);
|
|
138
138
|
}
|
|
@@ -144,7 +144,9 @@ class Remarkable {
|
|
|
144
144
|
}
|
|
145
145
|
async #putRootHash(hash, generation) {
|
|
146
146
|
try {
|
|
147
|
-
|
|
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
|
|
@@ -158,7 +160,7 @@ class Remarkable {
|
|
|
158
160
|
const resp = await fetch(url, {
|
|
159
161
|
method,
|
|
160
162
|
headers: {
|
|
161
|
-
Authorization: `Bearer ${this.#
|
|
163
|
+
Authorization: `Bearer ${this.#sessionToken}`,
|
|
162
164
|
...headers,
|
|
163
165
|
},
|
|
164
166
|
// fetch works correctly with uint8 arrays, but is not hinted correctly
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -596,16 +595,13 @@ class Remarkable {
|
|
|
596
595
|
}
|
|
597
596
|
const cached = values(nullable(string()));
|
|
598
597
|
/**
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
* This gets a temporary authentication token with the device token. If
|
|
602
|
-
* requests start failing, simply recreate the api instance.
|
|
598
|
+
* Exchange a device token for a session token.
|
|
603
599
|
*
|
|
604
600
|
* @param deviceToken - the device token proving this api instance is
|
|
605
601
|
* registered. Create one with {@link register}.
|
|
606
|
-
* @returns
|
|
602
|
+
* @returns the session token returned by the reMarkable service
|
|
607
603
|
*/
|
|
608
|
-
export async function
|
|
604
|
+
export async function auth(deviceToken, { authHost = AUTH_HOST } = {}) {
|
|
609
605
|
const resp = await fetch(`${authHost}/token/json/2/user/new`, {
|
|
610
606
|
method: "POST",
|
|
611
607
|
headers: {
|
|
@@ -615,16 +611,46 @@ export async function remarkable(deviceToken, { authHost = AUTH_HOST, rawHost =
|
|
|
615
611
|
if (!resp.ok) {
|
|
616
612
|
throw new Error(`couldn't fetch auth token: ${resp.statusText}`);
|
|
617
613
|
}
|
|
618
|
-
|
|
614
|
+
return await resp.text();
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Create an API instance from an existing session token.
|
|
618
|
+
*
|
|
619
|
+
* If requests start failing, simply recreate the api instance with a freshly
|
|
620
|
+
* fetched session token.
|
|
621
|
+
*
|
|
622
|
+
* @param sessionToken - the session token used for authorization
|
|
623
|
+
* @returns an api instance
|
|
624
|
+
*/
|
|
625
|
+
export function session(sessionToken, { rawHost = RAW_HOST, uploadHost = UPLOAD_HOST, cache, maxCacheSize = Infinity, } = {}) {
|
|
619
626
|
const initCache = JSON.parse(cache ?? "{}");
|
|
620
627
|
if (cached.guard(initCache)) {
|
|
621
628
|
const entries = Object.entries(initCache);
|
|
622
|
-
const
|
|
629
|
+
const cacheMap = maxCacheSize === Infinity
|
|
623
630
|
? new Map(entries)
|
|
624
631
|
: new LruCache(maxCacheSize, entries);
|
|
625
|
-
return new Remarkable(
|
|
626
|
-
}
|
|
627
|
-
else {
|
|
628
|
-
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.");
|
|
632
|
+
return new Remarkable(sessionToken, rawHost, uploadHost, cacheMap);
|
|
629
633
|
}
|
|
634
|
+
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.");
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* create an instance of the api
|
|
638
|
+
*
|
|
639
|
+
* This gets a temporary authentication token with the device token and then
|
|
640
|
+
* constructs the api instance.
|
|
641
|
+
*
|
|
642
|
+
* @param deviceToken - the device token proving this api instance is
|
|
643
|
+
* registered. Create one with {@link register}.
|
|
644
|
+
* @returns an api instance
|
|
645
|
+
*/
|
|
646
|
+
export async function remarkable(deviceToken, options = {}) {
|
|
647
|
+
const { authHost, rawHost, uploadHost, cache, maxCacheSize, syncHost } = options ?? {};
|
|
648
|
+
const sessionToken = await auth(deviceToken, { authHost });
|
|
649
|
+
return session(sessionToken, {
|
|
650
|
+
rawHost,
|
|
651
|
+
uploadHost,
|
|
652
|
+
cache,
|
|
653
|
+
maxCacheSize,
|
|
654
|
+
syncHost,
|
|
655
|
+
});
|
|
630
656
|
}
|
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
|
|
21
|
-
/** collection type
|
|
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 */
|
|
@@ -61,7 +62,7 @@ export interface PageTag extends Tag {
|
|
|
61
62
|
/** all supported document orientations */
|
|
62
63
|
export type Orientation = "portrait" | "landscape";
|
|
63
64
|
/** all supported text alignments */
|
|
64
|
-
export type TextAlignment = "justify" | "left";
|
|
65
|
+
export type TextAlignment = "" | "justify" | "left";
|
|
65
66
|
/** types of zoom modes for documents, applies primarily to pdf files */
|
|
66
67
|
export type ZoomMode = "bestFit" | "customFit" | "fitToHeight" | "fitToWidth";
|
|
67
68
|
/**
|
|
@@ -202,8 +203,8 @@ export interface DocumentContent {
|
|
|
202
203
|
pageCount: number;
|
|
203
204
|
/** the page tags for the document */
|
|
204
205
|
pageTags?: PageTag[];
|
|
205
|
-
/** a list of the ids of each page in the document */
|
|
206
|
-
pages?: string[];
|
|
206
|
+
/** a list of the ids of each page in the document, or null when never opened */
|
|
207
|
+
pages?: string[] | null;
|
|
207
208
|
/** a mapping from page number to page id in pages */
|
|
208
209
|
redirectionPageMap?: number[];
|
|
209
210
|
/** ostensibly the size in bytes of the file, but this differs from other measurements */
|
|
@@ -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<
|
|
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<[
|
|
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<[
|
|
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<[
|
|
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<[
|
|
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<[
|
|
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
|
*
|
|
@@ -554,28 +555,26 @@ export interface RawRemarkableApi {
|
|
|
554
555
|
/** completely clear the cache */
|
|
555
556
|
clearCache(): void;
|
|
556
557
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}): Promise<Response>;
|
|
562
|
-
}
|
|
558
|
+
type AuthedFetch = (method: RequestMethod, url: string, init?: {
|
|
559
|
+
body?: string | Uint8Array;
|
|
560
|
+
headers?: Record<string, string>;
|
|
561
|
+
}) => Promise<Response>;
|
|
563
562
|
export declare class RawRemarkable implements RawRemarkableApi {
|
|
564
563
|
#private;
|
|
565
564
|
constructor(authedFetch: AuthedFetch, cache: Map<string, string | null>, rawHost: string, uploadHost: string);
|
|
566
565
|
/** make an authorized request to remarkable */
|
|
567
|
-
getRootHash(): Promise<[string, number]>;
|
|
566
|
+
getRootHash(): Promise<[string, number, SchemaVersion]>;
|
|
568
567
|
getHash(hash: string): Promise<Uint8Array>;
|
|
569
568
|
getText(hash: string): Promise<string>;
|
|
570
|
-
getEntries(hash: string): Promise<
|
|
569
|
+
getEntries(hash: string): Promise<Entries>;
|
|
571
570
|
getContent(hash: string): Promise<Content>;
|
|
572
571
|
getMetadata(hash: string): Promise<Metadata>;
|
|
573
572
|
putRootHash(hash: string, generation: number, broadcast?: boolean): Promise<[string, number]>;
|
|
574
|
-
putFile(id: string, bytes: Uint8Array): Promise<[
|
|
575
|
-
putText(id: string, text: string): Promise<[
|
|
576
|
-
putContent(id: string, content: Content): Promise<[
|
|
577
|
-
putMetadata(id: string, metadata: Metadata): Promise<[
|
|
578
|
-
putEntries(id: string, entries: RawEntry[]): Promise<[
|
|
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>]>;
|
|
579
578
|
uploadFile(visibleName: string, bytes: Uint8Array, mime: UploadMimeType): Promise<SimpleEntry>;
|
|
580
579
|
dumpCache(): string;
|
|
581
580
|
clearCache(): void;
|