@profullstack/readm3 0.15.0 → 0.17.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 +56 -8
- package/dist/blob-store.d.ts +59 -0
- package/dist/blob-store.d.ts.map +1 -0
- package/dist/blob-store.js +95 -0
- package/dist/blob-store.js.map +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/sync-crypto.d.ts +43 -0
- package/dist/sync-crypto.d.ts.map +1 -0
- package/dist/sync-crypto.js +72 -0
- package/dist/sync-crypto.js.map +1 -0
- package/dist/sync-merge.d.ts +41 -0
- package/dist/sync-merge.d.ts.map +1 -0
- package/dist/sync-merge.js +86 -0
- package/dist/sync-merge.js.map +1 -0
- package/dist/sync-schema.d.ts +6 -1
- package/dist/sync-schema.d.ts.map +1 -1
- package/dist/sync-schema.js +36 -6
- package/dist/sync-schema.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -371,7 +371,31 @@ work offline. Remote URLs
|
|
|
371
371
|
need a connection and a host that permits browser access (CORS); files are fetched
|
|
372
372
|
directly with no credentials or server proxy. Local files are uploaded only when you explicitly choose Share → Save online.
|
|
373
373
|
|
|
374
|
-
|
|
374
|
+
#### File limits and storage
|
|
375
|
+
|
|
376
|
+
| | Without an account | Free account | Pay as you go |
|
|
377
|
+
| --- | --- | --- | --- |
|
|
378
|
+
| Largest file | 4 MB | 5 GB | 5 GB |
|
|
379
|
+
| Number of files | 1,000 (20 MB in all) | 5,000 | 5,000 |
|
|
380
|
+
| Synced storage | none | 5 GB | $0.05 per GB a month over 5 GB |
|
|
381
|
+
|
|
382
|
+
The meter beside Sync in the editor's header shows storage and files against these
|
|
383
|
+
limits; click it for the plan, the price and the balance, and to add funds.
|
|
384
|
+
|
|
385
|
+
Signed in, a file over 4 MB is a *stored file*: it opens read-only (a PDF or image is
|
|
386
|
+
shown whole, text as its first megabyte) and Download gives it back exactly. On the
|
|
387
|
+
device it is kept in browser storage as it is; on the account it is end-to-end
|
|
388
|
+
encrypted with its own key in 8 MB chunks, and the key travels only inside your
|
|
389
|
+
sealed workspace, so readm3.com holds data it cannot read or name. Small files that
|
|
390
|
+
do not fit the synced copy are stored the same way and turn back into ordinary,
|
|
391
|
+
editable files when opened.
|
|
392
|
+
|
|
393
|
+
Storage over the free 5 GB is charged hourly from the prepaid balance (CoinPay
|
|
394
|
+
top-ups, as for AI summaries). When the balance is empty, new uploads past the free
|
|
395
|
+
allowance are refused; nothing already stored is deleted. `GET /api/v1/blobs` reports
|
|
396
|
+
what an account stores, the allowance and the price. Self-hosted servers set
|
|
397
|
+
`READM3_FREE_STORAGE_BYTES`, `READM3_STORAGE_MICROS_PER_GB_MONTH` and
|
|
398
|
+
`READM3_BLOB_DIR` (default: `blobs/` beside the database).
|
|
375
399
|
|
|
376
400
|
### File history and AI summaries
|
|
377
401
|
|
|
@@ -647,11 +671,34 @@ bun run test:site # local/offline reader plus browser sharing and admin workf
|
|
|
647
671
|
|
|
648
672
|
## Workspace and settings sync
|
|
649
673
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
674
|
+
Every signed-in device (browser tab, installed app, desktop app) syncs the workspace
|
|
675
|
+
and reader settings **automatically and end to end encrypted**. Turn it off for one
|
|
676
|
+
device with **Sync automatically on this device** in the Sync dialog.
|
|
677
|
+
|
|
678
|
+
- **Automatic**: on load, a few seconds after each edit, on focus, and every 30 s
|
|
679
|
+
(that check asks only for the latest revision number). Edits merge per file
|
|
680
|
+
(`src/sync-merge.ts`): a file changed on one device takes that change; changed on two,
|
|
681
|
+
the account copy keeps the name and the other is kept beside it as
|
|
682
|
+
`name (conflict <device> <date>).md`. A device that has never synced loads first and
|
|
683
|
+
never uploads its starter files over an account.
|
|
684
|
+
- **End-to-end encrypted** (`src/sync-crypto.ts`): the workspace and settings are
|
|
685
|
+
gzip-compressed, then sealed with AES-256-GCM under a key derived (PBKDF2-SHA256,
|
|
686
|
+
600,000 iterations, per-account salt) from your **sync passphrase**. readm3.com stores
|
|
687
|
+
one `sealed.json` per revision and cannot read file names, text or settings. You choose
|
|
688
|
+
the passphrase on the first device and type it once on each other one (the derived key
|
|
689
|
+
stays in that device's IndexedDB, non-extractable); a password manager can store it.
|
|
690
|
+
There is no reset: without it the synced copy cannot be opened, though every device
|
|
691
|
+
keeps its local files. **Change sync passphrase** re-seals the account copy; other
|
|
692
|
+
devices then ask for the new one. The first encrypted save deletes every readable
|
|
693
|
+
revision the server held, and the server refuses readable saves after that.
|
|
694
|
+
|
|
695
|
+
Shared documents continue to use their own document permissions and version history.
|
|
696
|
+
Sync requires a connection; the local reader still works offline. The CLI's
|
|
697
|
+
`readm3 save`/`load`/`sync` still move readable snapshots and are refused by an account
|
|
698
|
+
that syncs encrypted.
|
|
699
|
+
|
|
700
|
+
Sign in with verified email at `/account`, then create a personal API token in `/admin`
|
|
701
|
+
for the CLI.
|
|
655
702
|
|
|
656
703
|
The CLI, document commands, and MCP use one credential file, `cloud.json`, under
|
|
657
704
|
`$XDG_CONFIG_HOME/readm3` (normally `~/.config/readm3`). It is written with mode
|
|
@@ -690,8 +737,9 @@ conflicting local edits, including on a device that has never synced. `--force`
|
|
|
690
737
|
explicitly overrides a conflict. Replaced local files receive numbered `.bak-NNN`
|
|
691
738
|
backups. Browser replacements receive backups in IndexedDB, accessible through
|
|
692
739
|
**Sync → Local backups** for download or restoration. Clearing browser site data
|
|
693
|
-
also removes those local backups.
|
|
694
|
-
|
|
740
|
+
also removes those local backups. The CLI syncs a readable workspace of up to 4 MB
|
|
741
|
+
per file and 20 MB in total; the browser and desktop app sync up to 5,000 files of
|
|
742
|
+
up to 5 GB each (see File limits and storage).
|
|
695
743
|
|
|
696
744
|
The authenticated API exposes:
|
|
697
745
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Large files, stored apart from the synced workspace.
|
|
3
|
+
*
|
|
4
|
+
* A signed-in account may keep files of up to 5 GB, and any number of them. A file that
|
|
5
|
+
* large cannot live in the workspace JSON (a string, synced as one request), so it is
|
|
6
|
+
* a `blob` document: the workspace keeps a reference, and the bytes live on their own,
|
|
7
|
+
* in IndexedDB on the device and in fixed-size chunks on readm3.com.
|
|
8
|
+
*
|
|
9
|
+
* Every blob has its own random AES-256 key. The key travels only inside the reference,
|
|
10
|
+
* which travels only inside the sealed (end-to-end encrypted) workspace, so the server
|
|
11
|
+
* stores chunks it cannot read and cannot link to a file name. Chunk n is encrypted
|
|
12
|
+
* with AES-256-GCM under an IV that is n itself: unique, since no key is reused across
|
|
13
|
+
* blobs, and a chunk moved to another position fails authentication.
|
|
14
|
+
*
|
|
15
|
+
* Small text files that do not fit the snapshot are stored the same way ("spilled") and
|
|
16
|
+
* carry the document's digest, so the merge sees the same document either way.
|
|
17
|
+
*
|
|
18
|
+
* Browser-safe: Web Crypto only. Shared by the page, the server and the tests.
|
|
19
|
+
*/
|
|
20
|
+
export declare const GiB: number;
|
|
21
|
+
/** The largest single file an account may keep. */
|
|
22
|
+
export declare const MAX_BLOB_BYTES: number;
|
|
23
|
+
/** Files in one signed-in workspace (signed out, the workspace keeps 1,000). */
|
|
24
|
+
export declare const MAX_ACCOUNT_FILES = 5000;
|
|
25
|
+
/** Storage every signed-in account gets free; more is pay as you go from the prepaid balance. */
|
|
26
|
+
export declare const FREE_STORAGE_BYTES: number;
|
|
27
|
+
/** Pay as you go: micro-dollars per GiB stored per month, over the free allowance. */
|
|
28
|
+
export declare const STORAGE_MICROS_PER_GIB_MONTH = 50000;
|
|
29
|
+
/** Plaintext bytes per chunk; each stored chunk is 16 bytes longer (the GCM tag). */
|
|
30
|
+
export declare const CHUNK_BYTES: number;
|
|
31
|
+
export declare const TAG_BYTES = 16;
|
|
32
|
+
/** Text larger than this opens read-only, as a preview; smaller text is edited in the workspace. */
|
|
33
|
+
export declare const INLINE_MAX_BYTES: number;
|
|
34
|
+
/** Inline text kept inside one synced snapshot; the rest of the small files spill to blobs. */
|
|
35
|
+
export declare const SNAPSHOT_INLINE_BYTES: number;
|
|
36
|
+
export interface BlobRef {
|
|
37
|
+
/** 22 url-safe characters: random, never derived from the content. */
|
|
38
|
+
id: string;
|
|
39
|
+
/** Plaintext bytes. */
|
|
40
|
+
size: number;
|
|
41
|
+
/** Base64 of the 32-byte AES key. */
|
|
42
|
+
key: string;
|
|
43
|
+
/** Set on a spilled text file: the document digest, so it compares equal to the inline copy. */
|
|
44
|
+
digest?: string;
|
|
45
|
+
}
|
|
46
|
+
export declare const isBlobId: (value: unknown) => value is string;
|
|
47
|
+
export declare function blobRefProblem(value: unknown): string | null;
|
|
48
|
+
export declare function newBlobRef(size: number, digest?: string): BlobRef;
|
|
49
|
+
export declare const chunkCount: (size: number) => number;
|
|
50
|
+
/** Plaintext bytes in chunk n of a blob of `size` bytes. */
|
|
51
|
+
export declare const chunkPlainBytes: (size: number, n: number) => number;
|
|
52
|
+
/** What the server stores for chunk n: the ciphertext and its tag. */
|
|
53
|
+
export declare const chunkStoredBytes: (size: number, n: number) => number;
|
|
54
|
+
/** Everything the server holds for a blob, which is what the account's storage counts. */
|
|
55
|
+
export declare const storedBytes: (size: number) => number;
|
|
56
|
+
export declare function encryptChunk(ref: BlobRef, n: number, plain: Uint8Array): Promise<Uint8Array>;
|
|
57
|
+
export declare function decryptChunk(ref: BlobRef, n: number, stored: Uint8Array): Promise<Uint8Array>;
|
|
58
|
+
export declare const formatStorage: (bytes: number) => string;
|
|
59
|
+
//# sourceMappingURL=blob-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blob-store.d.ts","sourceRoot":"","sources":["../src/blob-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,GAAG,QAAY,CAAC;AAC7B,mDAAmD;AACnD,eAAO,MAAM,cAAc,QAAU,CAAC;AACtC,gFAAgF;AAChF,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,iGAAiG;AACjG,eAAO,MAAM,kBAAkB,QAAU,CAAC;AAC1C,sFAAsF;AACtF,eAAO,MAAM,4BAA4B,QAAS,CAAC;AACnD,qFAAqF;AACrF,eAAO,MAAM,WAAW,QAAkB,CAAC;AAC3C,eAAO,MAAM,SAAS,KAAK,CAAC;AAC5B,oGAAoG;AACpG,eAAO,MAAM,gBAAgB,QAAkB,CAAC;AAChD,+FAA+F;AAC/F,eAAO,MAAM,qBAAqB,QAAmB,CAAC;AAEtD,MAAM,WAAW,OAAO;IACtB,sEAAsE;IACtE,EAAE,EAAE,MAAM,CAAC;IACX,uBAAuB;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,gGAAgG;IAChG,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAID,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MAAqD,CAAC;AAEzG,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAS5D;AASD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,MAAoD,CAAC;AAC/F,4DAA4D;AAC5D,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,EAAE,GAAG,MAAM,KAAG,MAAoE,CAAC;AAC/H,sEAAsE;AACtE,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,EAAE,GAAG,MAAM,KAAG,MAA8C,CAAC;AAC1G,0FAA0F;AAC1F,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,KAAG,MAA6C,CAAC;AAUzF,wBAAsB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAElG;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAGnG;AAED,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,MAC+H,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Large files, stored apart from the synced workspace.
|
|
3
|
+
*
|
|
4
|
+
* A signed-in account may keep files of up to 5 GB, and any number of them. A file that
|
|
5
|
+
* large cannot live in the workspace JSON (a string, synced as one request), so it is
|
|
6
|
+
* a `blob` document: the workspace keeps a reference, and the bytes live on their own,
|
|
7
|
+
* in IndexedDB on the device and in fixed-size chunks on readm3.com.
|
|
8
|
+
*
|
|
9
|
+
* Every blob has its own random AES-256 key. The key travels only inside the reference,
|
|
10
|
+
* which travels only inside the sealed (end-to-end encrypted) workspace, so the server
|
|
11
|
+
* stores chunks it cannot read and cannot link to a file name. Chunk n is encrypted
|
|
12
|
+
* with AES-256-GCM under an IV that is n itself: unique, since no key is reused across
|
|
13
|
+
* blobs, and a chunk moved to another position fails authentication.
|
|
14
|
+
*
|
|
15
|
+
* Small text files that do not fit the snapshot are stored the same way ("spilled") and
|
|
16
|
+
* carry the document's digest, so the merge sees the same document either way.
|
|
17
|
+
*
|
|
18
|
+
* Browser-safe: Web Crypto only. Shared by the page, the server and the tests.
|
|
19
|
+
*/
|
|
20
|
+
export const GiB = 1024 ** 3;
|
|
21
|
+
/** The largest single file an account may keep. */
|
|
22
|
+
export const MAX_BLOB_BYTES = 5 * GiB;
|
|
23
|
+
/** Files in one signed-in workspace (signed out, the workspace keeps 1,000). */
|
|
24
|
+
export const MAX_ACCOUNT_FILES = 5000;
|
|
25
|
+
/** Storage every signed-in account gets free; more is pay as you go from the prepaid balance. */
|
|
26
|
+
export const FREE_STORAGE_BYTES = 5 * GiB;
|
|
27
|
+
/** Pay as you go: micro-dollars per GiB stored per month, over the free allowance. */
|
|
28
|
+
export const STORAGE_MICROS_PER_GIB_MONTH = 50_000;
|
|
29
|
+
/** Plaintext bytes per chunk; each stored chunk is 16 bytes longer (the GCM tag). */
|
|
30
|
+
export const CHUNK_BYTES = 8 * 1024 * 1024;
|
|
31
|
+
export const TAG_BYTES = 16;
|
|
32
|
+
/** Text larger than this opens read-only, as a preview; smaller text is edited in the workspace. */
|
|
33
|
+
export const INLINE_MAX_BYTES = 4 * 1024 * 1024;
|
|
34
|
+
/** Inline text kept inside one synced snapshot; the rest of the small files spill to blobs. */
|
|
35
|
+
export const SNAPSHOT_INLINE_BYTES = 16 * 1024 * 1024;
|
|
36
|
+
const ID = /^[A-Za-z0-9_-]{22}$/;
|
|
37
|
+
const KEY = /^[A-Za-z0-9+/]{43}=$/;
|
|
38
|
+
export const isBlobId = (value) => typeof value === "string" && ID.test(value);
|
|
39
|
+
export function blobRefProblem(value) {
|
|
40
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
41
|
+
return "A stored file reference must be an object.";
|
|
42
|
+
const ref = value;
|
|
43
|
+
if (Object.keys(ref).some((key) => !["id", "size", "key", "digest"].includes(key)))
|
|
44
|
+
return "A stored file reference has unknown fields.";
|
|
45
|
+
if (!isBlobId(ref.id))
|
|
46
|
+
return "A stored file has an invalid id.";
|
|
47
|
+
if (!Number.isSafeInteger(ref.size) || ref.size < 0 || ref.size > MAX_BLOB_BYTES)
|
|
48
|
+
return "A stored file is larger than 5 GB.";
|
|
49
|
+
if (typeof ref.key !== "string" || !KEY.test(ref.key))
|
|
50
|
+
return "A stored file has an invalid key.";
|
|
51
|
+
if (ref.digest !== undefined && (typeof ref.digest !== "string" || !/^[a-z0-9]{1,16}:[a-z0-9]{1,16}$/.test(ref.digest)))
|
|
52
|
+
return "A stored file has an invalid digest.";
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const b64 = (bytes) => {
|
|
56
|
+
let text = "";
|
|
57
|
+
for (let i = 0; i < bytes.length; i += 0x8000)
|
|
58
|
+
text += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
59
|
+
return btoa(text);
|
|
60
|
+
};
|
|
61
|
+
const unb64 = (text) => Uint8Array.from(atob(text), (c) => c.charCodeAt(0));
|
|
62
|
+
export function newBlobRef(size, digest) {
|
|
63
|
+
const id = b64(crypto.getRandomValues(new Uint8Array(16))).replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
64
|
+
return { id, size, key: b64(crypto.getRandomValues(new Uint8Array(32))), ...(digest ? { digest } : {}) };
|
|
65
|
+
}
|
|
66
|
+
export const chunkCount = (size) => Math.max(1, Math.ceil(size / CHUNK_BYTES));
|
|
67
|
+
/** Plaintext bytes in chunk n of a blob of `size` bytes. */
|
|
68
|
+
export const chunkPlainBytes = (size, n) => Math.max(0, Math.min(CHUNK_BYTES, size - n * CHUNK_BYTES));
|
|
69
|
+
/** What the server stores for chunk n: the ciphertext and its tag. */
|
|
70
|
+
export const chunkStoredBytes = (size, n) => chunkPlainBytes(size, n) + TAG_BYTES;
|
|
71
|
+
/** Everything the server holds for a blob, which is what the account's storage counts. */
|
|
72
|
+
export const storedBytes = (size) => size + chunkCount(size) * TAG_BYTES;
|
|
73
|
+
const iv = (n) => { const bytes = new Uint8Array(12); new DataView(bytes.buffer).setUint32(8, n); return bytes; };
|
|
74
|
+
const keys = new Map();
|
|
75
|
+
const cryptoKey = (ref) => {
|
|
76
|
+
let key = keys.get(ref.key);
|
|
77
|
+
if (!key) {
|
|
78
|
+
key = crypto.subtle.importKey("raw", unb64(ref.key), "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
79
|
+
keys.set(ref.key, key);
|
|
80
|
+
}
|
|
81
|
+
return key;
|
|
82
|
+
};
|
|
83
|
+
export async function encryptChunk(ref, n, plain) {
|
|
84
|
+
return new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv(n) }, await cryptoKey(ref), plain));
|
|
85
|
+
}
|
|
86
|
+
export async function decryptChunk(ref, n, stored) {
|
|
87
|
+
try {
|
|
88
|
+
return new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv(n) }, await cryptoKey(ref), stored));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new Error("A stored file chunk failed to decrypt; it was damaged or replaced.");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export const formatStorage = (bytes) => bytes >= GiB ? `${(bytes / GiB).toFixed(bytes >= 10 * GiB ? 0 : 1)} GB` : bytes >= 1024 * 1024 ? `${(bytes / 1024 / 1024).toFixed(1)} MB` : `${Math.ceil(bytes / 1024)} KB`;
|
|
95
|
+
//# sourceMappingURL=blob-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blob-store.js","sourceRoot":"","sources":["../src/blob-store.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ColorMode } from "./print.ts";
|
|
2
2
|
import { type ViewerOptions } from "./viewer.ts";
|
|
3
|
-
export declare const VERSION = "0.
|
|
3
|
+
export declare const VERSION = "0.17.0";
|
|
4
4
|
/** The help text, exported so a test can hold it to the flags it documents. */
|
|
5
5
|
export declare const USAGE: string;
|
|
6
6
|
export interface ParsedArgs extends ViewerOptions {
|
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import { printDocument, COLOR_MODES } from "./print.js";
|
|
|
11
11
|
import { run } from "./viewer.js";
|
|
12
12
|
import { isCloudCommand, cloudMain, CLOUD_USAGE } from "./cloud-cli.js";
|
|
13
13
|
import { isDesktopCommand, desktopMain, installedDesktop, prefersDesktop, DESKTOP_USAGE } from "./desktop-cli.js";
|
|
14
|
-
export const VERSION = "0.
|
|
14
|
+
export const VERSION = "0.17.0";
|
|
15
15
|
/** The help text, exported so a test can hold it to the flags it documents. */
|
|
16
16
|
export const USAGE = `readm3 — a plain text editor for Markdown and code
|
|
17
17
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end encryption for synced workspaces. The server stores one sealed blob per
|
|
3
|
+
* revision and can read nothing in it: not the documents, their names, or the settings.
|
|
4
|
+
*
|
|
5
|
+
* files JSON → gzip → AES-256-GCM (key from the sync passphrase) → base64 envelope
|
|
6
|
+
*
|
|
7
|
+
* The key is PBKDF2-SHA256 (600,000 iterations, a random 16-byte salt per account) over
|
|
8
|
+
* the person's sync passphrase, which never leaves the device. The salt and iteration
|
|
9
|
+
* count travel in each envelope so any device can derive the same key; a wrong
|
|
10
|
+
* passphrase fails GCM authentication rather than producing garbage. Web Crypto and
|
|
11
|
+
* CompressionStream only, so the same code runs in browsers, the desktop app, Node and Bun.
|
|
12
|
+
*/
|
|
13
|
+
export declare const SEALED_FILE = "sealed.json";
|
|
14
|
+
export declare const KDF_ITERATIONS = 600000;
|
|
15
|
+
export declare const MIN_PASSPHRASE = 10;
|
|
16
|
+
export interface Kdf {
|
|
17
|
+
name: "PBKDF2-SHA256";
|
|
18
|
+
iterations: number;
|
|
19
|
+
salt: string;
|
|
20
|
+
}
|
|
21
|
+
export interface Envelope {
|
|
22
|
+
v: 1;
|
|
23
|
+
alg: "AES-256-GCM";
|
|
24
|
+
zip: "gzip";
|
|
25
|
+
kdf: Kdf;
|
|
26
|
+
iv: string;
|
|
27
|
+
data: string;
|
|
28
|
+
}
|
|
29
|
+
export interface SyncKey {
|
|
30
|
+
key: CryptoKey;
|
|
31
|
+
kdf: Kdf;
|
|
32
|
+
}
|
|
33
|
+
export declare class WrongPassphrase extends Error {
|
|
34
|
+
constructor();
|
|
35
|
+
}
|
|
36
|
+
/** A new account key: fresh salt. */
|
|
37
|
+
export declare const newKdf: () => Kdf;
|
|
38
|
+
export declare function deriveKey(passphrase: string, kdf: Kdf): Promise<SyncKey>;
|
|
39
|
+
export declare function seal(value: unknown, { key, kdf }: SyncKey): Promise<Envelope>;
|
|
40
|
+
export declare function open<T = unknown>(envelope: Envelope, { key }: SyncKey): Promise<T>;
|
|
41
|
+
/** The envelope's shape, checked on the server, which cannot check anything inside it. */
|
|
42
|
+
export declare function envelopeProblem(value: unknown): string | null;
|
|
43
|
+
//# sourceMappingURL=sync-crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-crypto.d.ts","sourceRoot":"","sources":["../src/sync-crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW,gBAAgB,CAAC;AACzC,eAAO,MAAM,cAAc,SAAU,CAAC;AACtC,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,MAAM,WAAW,GAAG;IAAG,IAAI,EAAE,eAAe,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAChF,MAAM,WAAW,QAAQ;IAAG,CAAC,EAAE,CAAC,CAAC;IAAC,GAAG,EAAE,aAAa,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,GAAG,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AACvG,MAAM,WAAW,OAAO;IAAG,GAAG,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE;AAErD,qBAAa,eAAgB,SAAQ,KAAK;;CAEzC;AAaD,qCAAqC;AACrC,eAAO,MAAM,MAAM,QAAO,GAAqH,CAAC;AAEhJ,wBAAsB,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,CAS9E;AAED,wBAAsB,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAKnF;AAED,wBAAsB,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAKxF;AAED,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAU7D"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end encryption for synced workspaces. The server stores one sealed blob per
|
|
3
|
+
* revision and can read nothing in it: not the documents, their names, or the settings.
|
|
4
|
+
*
|
|
5
|
+
* files JSON → gzip → AES-256-GCM (key from the sync passphrase) → base64 envelope
|
|
6
|
+
*
|
|
7
|
+
* The key is PBKDF2-SHA256 (600,000 iterations, a random 16-byte salt per account) over
|
|
8
|
+
* the person's sync passphrase, which never leaves the device. The salt and iteration
|
|
9
|
+
* count travel in each envelope so any device can derive the same key; a wrong
|
|
10
|
+
* passphrase fails GCM authentication rather than producing garbage. Web Crypto and
|
|
11
|
+
* CompressionStream only, so the same code runs in browsers, the desktop app, Node and Bun.
|
|
12
|
+
*/
|
|
13
|
+
export const SEALED_FILE = "sealed.json";
|
|
14
|
+
export const KDF_ITERATIONS = 600_000;
|
|
15
|
+
export const MIN_PASSPHRASE = 10;
|
|
16
|
+
export class WrongPassphrase extends Error {
|
|
17
|
+
constructor() { super("That passphrase does not open this account's synced files."); }
|
|
18
|
+
}
|
|
19
|
+
const b64 = (bytes) => {
|
|
20
|
+
let text = "";
|
|
21
|
+
for (let i = 0; i < bytes.length; i += 0x8000)
|
|
22
|
+
text += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
23
|
+
return btoa(text);
|
|
24
|
+
};
|
|
25
|
+
const unb64 = (text) => Uint8Array.from(atob(text), (c) => c.charCodeAt(0));
|
|
26
|
+
async function pipe(bytes, stream) {
|
|
27
|
+
return new Uint8Array(await new Response(new Blob([bytes]).stream().pipeThrough(stream)).arrayBuffer());
|
|
28
|
+
}
|
|
29
|
+
/** A new account key: fresh salt. */
|
|
30
|
+
export const newKdf = () => ({ name: "PBKDF2-SHA256", iterations: KDF_ITERATIONS, salt: b64(crypto.getRandomValues(new Uint8Array(16))) });
|
|
31
|
+
export async function deriveKey(passphrase, kdf) {
|
|
32
|
+
if (kdf.name !== "PBKDF2-SHA256" || !Number.isSafeInteger(kdf.iterations) || kdf.iterations < 100_000 || kdf.iterations > 10_000_000)
|
|
33
|
+
throw new Error("Unsupported key derivation in the synced files.");
|
|
34
|
+
const material = await crypto.subtle.importKey("raw", new TextEncoder().encode(passphrase.normalize("NFC")), "PBKDF2", false, ["deriveKey"]);
|
|
35
|
+
const key = await crypto.subtle.deriveKey({ name: "PBKDF2", hash: "SHA-256", salt: unb64(kdf.salt), iterations: kdf.iterations }, material, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
36
|
+
return { key, kdf };
|
|
37
|
+
}
|
|
38
|
+
export async function seal(value, { key, kdf }) {
|
|
39
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
40
|
+
const packed = await pipe(new TextEncoder().encode(JSON.stringify(value)), new CompressionStream("gzip"));
|
|
41
|
+
const sealed = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv }, key, packed));
|
|
42
|
+
return { v: 1, alg: "AES-256-GCM", zip: "gzip", kdf, iv: b64(iv), data: b64(sealed) };
|
|
43
|
+
}
|
|
44
|
+
export async function open(envelope, { key }) {
|
|
45
|
+
let packed;
|
|
46
|
+
try {
|
|
47
|
+
packed = new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: unb64(envelope.iv) }, key, unb64(envelope.data)));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new WrongPassphrase();
|
|
51
|
+
}
|
|
52
|
+
return JSON.parse(new TextDecoder().decode(await pipe(packed, new DecompressionStream("gzip"))));
|
|
53
|
+
}
|
|
54
|
+
/** The envelope's shape, checked on the server, which cannot check anything inside it. */
|
|
55
|
+
export function envelopeProblem(value) {
|
|
56
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
57
|
+
return "sealed.json must be an object";
|
|
58
|
+
const e = value;
|
|
59
|
+
if (Object.keys(e).some((key) => !["v", "alg", "zip", "kdf", "iv", "data"].includes(key)))
|
|
60
|
+
return "sealed.json has unknown fields";
|
|
61
|
+
if (e.v !== 1 || e.alg !== "AES-256-GCM" || e.zip !== "gzip")
|
|
62
|
+
return "unsupported sealed.json format";
|
|
63
|
+
const kdf = e.kdf;
|
|
64
|
+
if (!kdf || kdf.name !== "PBKDF2-SHA256" || !Number.isSafeInteger(kdf.iterations) || typeof kdf.salt !== "string" || !/^[A-Za-z0-9+/]{22}==$/.test(kdf.salt))
|
|
65
|
+
return "invalid key derivation";
|
|
66
|
+
if (typeof e.iv !== "string" || !/^[A-Za-z0-9+/]{16}$/.test(e.iv))
|
|
67
|
+
return "invalid iv";
|
|
68
|
+
if (typeof e.data !== "string" || !/^[A-Za-z0-9+/]+={0,2}$/.test(e.data))
|
|
69
|
+
return "invalid sealed data";
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=sync-crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-crypto.js","sourceRoot":"","sources":["../src/sync-crypto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three-way merge of a synced workspace, per document, so every signed-in device can
|
|
3
|
+
* sync on its own without one device's copy replacing another's.
|
|
4
|
+
*
|
|
5
|
+
* - `base` is what this device last agreed with the account: a digest per path.
|
|
6
|
+
* - A document changed only on one side takes that side; changed on both, the account
|
|
7
|
+
* copy keeps the path and this device's copy is kept beside it as a conflict file.
|
|
8
|
+
* - A deletion carries over unless the other side edited the document meanwhile.
|
|
9
|
+
* - A device that has never synced (no base) never deletes or overwrites anything:
|
|
10
|
+
* the account's documents win their paths, and the device's own documents join them,
|
|
11
|
+
* except the untouched starter documents every new device begins with.
|
|
12
|
+
*
|
|
13
|
+
* Pure (no DOM, no network) so it is tested like the renderer.
|
|
14
|
+
*/
|
|
15
|
+
import type { SyncedWorkspace, WorkspaceDocument } from "./sync-schema.ts";
|
|
16
|
+
export type Digest = (text: string) => string;
|
|
17
|
+
/**
|
|
18
|
+
* FNV-1a over the text and the language: cheap, and only ever compared on one account.
|
|
19
|
+
* A spilled text file carries the digest of its text; a large stored file is its blob.
|
|
20
|
+
*/
|
|
21
|
+
export declare const digestDocument: (doc: Pick<WorkspaceDocument, "source" | "language" | "blob">) => string;
|
|
22
|
+
export declare const digests: (workspace: SyncedWorkspace | undefined) => Record<string, string>;
|
|
23
|
+
export interface MergeResult {
|
|
24
|
+
workspace: SyncedWorkspace;
|
|
25
|
+
/** Paths saved beside the account copy because both sides changed them. */
|
|
26
|
+
conflicts: string[];
|
|
27
|
+
/** The merge differs from what the account holds, so this device must upload it. */
|
|
28
|
+
upload: boolean;
|
|
29
|
+
/** The merge differs from what this device holds, so it must be applied here. */
|
|
30
|
+
download: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare function mergeWorkspaces(options: {
|
|
33
|
+
local: SyncedWorkspace;
|
|
34
|
+
remote: SyncedWorkspace | undefined;
|
|
35
|
+
base: Record<string, string> | undefined;
|
|
36
|
+
/** Starter documents (path → digest) that a fresh device holds and nobody wrote. */
|
|
37
|
+
starter?: Record<string, string>;
|
|
38
|
+
/** Names this device in conflict copies, e.g. "conflict desktop 2026-10-19". */
|
|
39
|
+
conflictLabel?: string;
|
|
40
|
+
}): MergeResult;
|
|
41
|
+
//# sourceMappingURL=sync-merge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-merge.d.ts","sourceRoot":"","sources":["../src/sync-merge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE3E,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;AAE9C;;;GAGG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,iBAAiB,EAAE,QAAQ,GAAG,UAAU,GAAG,MAAM,CAAC,KAAG,MAM7F,CAAC;AAEF,eAAO,MAAM,OAAO,GAAI,WAAW,eAAe,GAAG,SAAS,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CACU,CAAC;AAEjG,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,eAAe,CAAC;IAC3B,2EAA2E;IAC3E,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,oFAAoF;IACpF,MAAM,EAAE,OAAO,CAAC;IAChB,iFAAiF;IACjF,QAAQ,EAAE,OAAO,CAAC;CACnB;AAYD,wBAAgB,eAAe,CAAC,OAAO,EAAE;IACvC,KAAK,EAAE,eAAe,CAAC;IACvB,MAAM,EAAE,eAAe,GAAG,SAAS,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IACzC,oFAAoF;IACpF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,WAAW,CAiDd"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FNV-1a over the text and the language: cheap, and only ever compared on one account.
|
|
3
|
+
* A spilled text file carries the digest of its text; a large stored file is its blob.
|
|
4
|
+
*/
|
|
5
|
+
export const digestDocument = (doc) => {
|
|
6
|
+
if (doc.blob)
|
|
7
|
+
return doc.blob.digest ?? `blob:${doc.blob.id}`;
|
|
8
|
+
let hash = 0x811c9dc5;
|
|
9
|
+
const text = `${doc.language ?? ""}\0${doc.source}`;
|
|
10
|
+
for (let i = 0; i < text.length; i++)
|
|
11
|
+
hash = Math.imul(hash ^ text.charCodeAt(i), 0x01000193);
|
|
12
|
+
return `${text.length.toString(36)}:${(hash >>> 0).toString(36)}`;
|
|
13
|
+
};
|
|
14
|
+
export const digests = (workspace) => Object.fromEntries((workspace?.documents ?? []).map((doc) => [doc.path, digestDocument(doc)]));
|
|
15
|
+
function conflictPath(path, label, taken) {
|
|
16
|
+
const slash = path.lastIndexOf("/");
|
|
17
|
+
const dot = path.lastIndexOf(".");
|
|
18
|
+
const cut = dot > slash + 1 ? dot : path.length;
|
|
19
|
+
for (let n = 1;; n++) {
|
|
20
|
+
const candidate = `${path.slice(0, cut)} (${label}${n > 1 ? ` ${n}` : ""})${path.slice(cut)}`;
|
|
21
|
+
if (!taken.has(candidate))
|
|
22
|
+
return candidate;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function mergeWorkspaces(options) {
|
|
26
|
+
const { local, remote, base, starter = {}, conflictLabel = "conflict" } = options;
|
|
27
|
+
const L = new Map(local.documents.map((doc) => [doc.path, doc]));
|
|
28
|
+
const R = new Map((remote?.documents ?? []).map((doc) => [doc.path, doc]));
|
|
29
|
+
const merged = new Map();
|
|
30
|
+
const conflicts = [];
|
|
31
|
+
const isStarter = (doc) => starter[doc.path] === digestDocument(doc);
|
|
32
|
+
for (const path of new Set([...L.keys(), ...R.keys()])) {
|
|
33
|
+
const l = L.get(path), r = R.get(path);
|
|
34
|
+
const ld = l && digestDocument(l), rd = r && digestDocument(r);
|
|
35
|
+
if (!base) {
|
|
36
|
+
// First sync on this device: nothing is deleted, the account wins shared paths.
|
|
37
|
+
if (r)
|
|
38
|
+
merged.set(path, r);
|
|
39
|
+
if (l && !isStarter(l) && (!r || ld !== rd)) {
|
|
40
|
+
if (!r)
|
|
41
|
+
merged.set(path, l);
|
|
42
|
+
else {
|
|
43
|
+
const copy = conflictPath(path, conflictLabel, new Set([...L.keys(), ...R.keys(), ...merged.keys()]));
|
|
44
|
+
merged.set(copy, { ...l, path: copy });
|
|
45
|
+
conflicts.push(copy);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const bd = base[path];
|
|
51
|
+
if (l && r) {
|
|
52
|
+
if (ld === rd || ld === bd)
|
|
53
|
+
merged.set(path, r);
|
|
54
|
+
else if (rd === bd)
|
|
55
|
+
merged.set(path, l);
|
|
56
|
+
else {
|
|
57
|
+
merged.set(path, r);
|
|
58
|
+
const copy = conflictPath(path, conflictLabel, new Set([...L.keys(), ...R.keys(), ...merged.keys()]));
|
|
59
|
+
merged.set(copy, { ...l, path: copy });
|
|
60
|
+
conflicts.push(copy);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (l) {
|
|
64
|
+
// Gone from the account: deleted elsewhere, unless it is new here or edited here since.
|
|
65
|
+
if (bd === undefined || ld !== bd)
|
|
66
|
+
merged.set(path, l);
|
|
67
|
+
}
|
|
68
|
+
else if (r) {
|
|
69
|
+
// Gone from this device: deleted here, unless it is new there or edited there since.
|
|
70
|
+
if (bd === undefined || rd !== bd)
|
|
71
|
+
merged.set(path, r);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const documents = [...merged.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
75
|
+
// A workspace never syncs empty; the schema needs one document and an active path.
|
|
76
|
+
if (!documents.length)
|
|
77
|
+
documents.push(...local.documents);
|
|
78
|
+
const name = remote && (!base ? true : local.name === remote.name) ? remote.name : local.name;
|
|
79
|
+
const active = merged.has(local.active) ? local.active : remote && merged.has(remote.active) ? remote.active : documents[0].path;
|
|
80
|
+
const workspace = { name, active, documents };
|
|
81
|
+
const same = (a) => !!a && a.name === workspace.name
|
|
82
|
+
&& a.documents.length === documents.length
|
|
83
|
+
&& documents.every((doc) => { const other = a.documents.find((d) => d.path === doc.path); return !!other && digestDocument(other) === digestDocument(doc); });
|
|
84
|
+
return { workspace, conflicts, upload: !same(remote), download: !same(local) };
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=sync-merge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-merge.js","sourceRoot":"","sources":["../src/sync-merge.ts"],"names":[],"mappings":"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"}
|
package/dist/sync-schema.d.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/** Browser-safe account data contract. Only these two files may be synced. */
|
|
2
2
|
import { type Flavor } from "./flavors.ts";
|
|
3
|
+
import { type BlobRef } from "./blob-store.ts";
|
|
3
4
|
/**
|
|
4
5
|
* A workspace file. `source` is the text, or base64 bytes when the path names a PDF or
|
|
5
6
|
* an image. `language` is only stored when the name could not decide it (no known
|
|
6
|
-
* extension) and the content was sniffed on import.
|
|
7
|
+
* extension) and the content was sniffed on import. A file stored apart (src/blob-store.ts:
|
|
8
|
+
* larger than 4 MB, or spilled from a full snapshot) has `blob` and an empty `source`.
|
|
7
9
|
*/
|
|
8
10
|
export interface WorkspaceDocument {
|
|
9
11
|
path: string;
|
|
10
12
|
source: string;
|
|
11
13
|
language?: string;
|
|
14
|
+
blob?: BlobRef;
|
|
12
15
|
}
|
|
13
16
|
export interface SyncedWorkspace {
|
|
14
17
|
documents: WorkspaceDocument[];
|
|
@@ -37,6 +40,8 @@ export declare const SYNC_POLICY: {
|
|
|
37
40
|
};
|
|
38
41
|
export declare function validateSettings(value: unknown): ReaderSettings;
|
|
39
42
|
export declare function validateWorkspace(value: unknown): SyncedWorkspace;
|
|
43
|
+
/** An end-to-end encrypted snapshot (src/sync-crypto.ts): one sealed.json, nothing readable beside it. */
|
|
44
|
+
export declare const isSealed: (files: Record<string, unknown> | undefined) => boolean;
|
|
40
45
|
export declare function validateSyncFiles(files: Record<string, {
|
|
41
46
|
content: string;
|
|
42
47
|
}>): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-schema.d.ts","sourceRoot":"","sources":["../src/sync-schema.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,OAAO,EAAY,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAErD
|
|
1
|
+
{"version":3,"file":"sync-schema.d.ts","sourceRoot":"","sources":["../src/sync-schema.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,OAAO,EAAY,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,EAA8E,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE3H;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE;AACtG,MAAM,WAAW,eAAe;IAAG,SAAS,EAAE,iBAAiB,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AACjG,MAAM,WAAW,cAAc;IAAG,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE;AACnE,eAAO,MAAM,WAAW;;;;CAAmF,CAAC;AAC5G,eAAO,MAAM,UAAU,8CAA+C,CAAC;AACvE,eAAO,MAAM,WAAW;;;;;;;;;CAIvB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAO/D;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CA4BjE;AAED,0GAA0G;AAC1G,eAAO,MAAM,QAAQ,GAAI,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,KAAG,OAA0C,CAAC;AAEjH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,CAiBlF"}
|
package/dist/sync-schema.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Browser-safe account data contract. Only these two files may be synced. */
|
|
2
2
|
import { isFlavor } from "./flavors.js";
|
|
3
|
+
import { envelopeProblem, SEALED_FILE } from "./sync-crypto.js";
|
|
4
|
+
import { blobRefProblem, INLINE_MAX_BYTES, MAX_ACCOUNT_FILES, SNAPSHOT_INLINE_BYTES } from "./blob-store.js";
|
|
3
5
|
export const SYNC_LIMITS = { maxFileBytes: 32 * 1024 * 1024, maxTotalBytes: 33 * 1024 * 1024, maxFiles: 2 };
|
|
4
6
|
export const SYNC_FILES = ["settings.json", "workspace.json"];
|
|
5
7
|
export const SYNC_POLICY = {
|
|
@@ -25,8 +27,8 @@ export function validateWorkspace(value) {
|
|
|
25
27
|
const ws = value;
|
|
26
28
|
if (Object.keys(ws).some((key) => !["name", "active", "documents"].includes(key)))
|
|
27
29
|
throw new Error("Workspace can only contain name, active and documents.");
|
|
28
|
-
if (typeof ws.name !== "string" || ws.name.length > 200 || !Array.isArray(ws.documents) || !ws.documents.length || ws.documents.length >
|
|
29
|
-
throw new Error("Workspace needs a name and 1–
|
|
30
|
+
if (typeof ws.name !== "string" || ws.name.length > 200 || !Array.isArray(ws.documents) || !ws.documents.length || ws.documents.length > MAX_ACCOUNT_FILES)
|
|
31
|
+
throw new Error("Workspace needs a name and 1–5,000 files.");
|
|
30
32
|
const paths = new Set();
|
|
31
33
|
let total = 0;
|
|
32
34
|
const encoder = new TextEncoder();
|
|
@@ -39,19 +41,47 @@ export function validateWorkspace(value) {
|
|
|
39
41
|
throw new Error("A file's source must be text.");
|
|
40
42
|
if (doc.language !== undefined && (typeof doc.language !== "string" || !/^[a-z0-9_+-]{1,40}$/.test(doc.language)))
|
|
41
43
|
throw new Error("A file's language must be a short highlight name.");
|
|
42
|
-
if (Object.keys(doc).some((key) => !["path", "source", "language"].includes(key)))
|
|
43
|
-
throw new Error("Documents can only contain path, source and
|
|
44
|
+
if (Object.keys(doc).some((key) => !["path", "source", "language", "blob"].includes(key)))
|
|
45
|
+
throw new Error("Documents can only contain path, source, language and blob.");
|
|
46
|
+
if (doc.blob !== undefined) {
|
|
47
|
+
const problem = blobRefProblem(doc.blob);
|
|
48
|
+
if (problem)
|
|
49
|
+
throw new Error(problem);
|
|
50
|
+
if (doc.source !== "")
|
|
51
|
+
throw new Error("A stored file keeps its bytes apart; its source must be empty.");
|
|
52
|
+
}
|
|
44
53
|
const bytes = encoder.encode(doc.source).length;
|
|
45
54
|
total += bytes;
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
// Larger files, and small ones past the snapshot's share, are stored apart (src/blob-store.ts).
|
|
56
|
+
if (bytes > INLINE_MAX_BYTES || total > SNAPSHOT_INLINE_BYTES + INLINE_MAX_BYTES)
|
|
57
|
+
throw new Error("Workspace limit: 4 MB of text per file inside the synced copy; larger files are stored apart.");
|
|
48
58
|
paths.add(doc.path);
|
|
49
59
|
}
|
|
50
60
|
if (!paths.has(ws.active))
|
|
51
61
|
throw new Error("The active document is missing.");
|
|
52
62
|
return ws;
|
|
53
63
|
}
|
|
64
|
+
/** An end-to-end encrypted snapshot (src/sync-crypto.ts): one sealed.json, nothing readable beside it. */
|
|
65
|
+
export const isSealed = (files) => !!files && SEALED_FILE in files;
|
|
54
66
|
export function validateSyncFiles(files) {
|
|
67
|
+
if (isSealed(files)) {
|
|
68
|
+
if (Object.keys(files).length !== 1)
|
|
69
|
+
throw new Error("An encrypted snapshot holds sealed.json and nothing else.");
|
|
70
|
+
const file = files[SEALED_FILE];
|
|
71
|
+
if (!file || typeof file.content !== "string")
|
|
72
|
+
throw new Error("sealed.json: content must be text.");
|
|
73
|
+
let envelope;
|
|
74
|
+
try {
|
|
75
|
+
envelope = JSON.parse(file.content);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw new Error("sealed.json is not JSON.");
|
|
79
|
+
}
|
|
80
|
+
const problem = envelopeProblem(envelope);
|
|
81
|
+
if (problem)
|
|
82
|
+
throw new Error(problem);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
55
85
|
for (const [path, file] of Object.entries(files)) {
|
|
56
86
|
if (!SYNC_FILES.includes(path))
|
|
57
87
|
throw new Error(`File is not syncable: ${path}`);
|
package/dist/sync-schema.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-schema.js","sourceRoot":"","sources":["../src/sync-schema.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,OAAO,EAAE,QAAQ,EAAe,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"sync-schema.js","sourceRoot":"","sources":["../src/sync-schema.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/readm3",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Markdown editing and sharing for people and teams. Web/PWA, CLI, API and MCP with version history, view/edit links, and a terminal reader.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|