canto-data 1.0.0 → 1.0.2
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 +259 -33
- package/package.json +14 -2
package/README.md
CHANGED
|
@@ -1,12 +1,48 @@
|
|
|
1
1
|
# canto-data
|
|
2
2
|
|
|
3
|
-
[
|
|
3
|
+
Data model library for [Canto](https://github.com/pboueke/canto), a private encrypted journaling app.
|
|
4
|
+
|
|
4
5
|
[](LICENSE)
|
|
5
|
-
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
[](https://github.com/pboueke/canto-data/actions/workflows/ci.yml)
|
|
10
|
+
[](https://github.com/pboueke/canto-data/actions/workflows/ci.yml)
|
|
11
|
+
|
|
12
|
+
`canto-data` provides TypeScript types, runtime validation, schema versioning, migration infrastructure, and export format utilities for Canto journals.
|
|
13
|
+
|
|
14
|
+
This package is **MIT-licensed** and has **zero dependencies**. It can be used independently of the Canto app to read, validate, and manipulate Canto journal data.
|
|
15
|
+
|
|
16
|
+
## Relationship to the Canto App
|
|
17
|
+
|
|
18
|
+
Canto (the app) is GPLv3-licensed. `canto-data` (this library) is MIT-licensed to enable data portability: anyone can build tools that interoperate with Canto journals without being bound by the app's copyleft license.
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
canto-data (MIT)
|
|
22
|
+
└── src/
|
|
23
|
+
├── types.ts # All TypeScript interfaces
|
|
24
|
+
├── validation.ts # Type guards and structural validators
|
|
25
|
+
├── version.ts # Schema version constant and semver utils
|
|
26
|
+
├── migration.ts # Forward-only migration runner
|
|
27
|
+
├── migrations/ # Migration registry
|
|
28
|
+
└── format.ts # Export manifest and ZIP format utilities
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
What `canto-data` owns:
|
|
32
|
+
|
|
33
|
+
- All journal data types (Journal, Page, Attachment, Comment, and related structures)
|
|
34
|
+
- Runtime validation and type guards
|
|
35
|
+
- Schema versioning and migration framework
|
|
36
|
+
- Export format specification (manifest structure and attachment naming)
|
|
6
37
|
|
|
7
|
-
|
|
38
|
+
What it does **not** include:
|
|
8
39
|
|
|
9
|
-
|
|
40
|
+
- Encryption and decryption
|
|
41
|
+
- Storage backends
|
|
42
|
+
- Sync integrations
|
|
43
|
+
- UI components
|
|
44
|
+
|
|
45
|
+
Those pieces live in the [Canto app](https://github.com/pboueke/canto).
|
|
10
46
|
|
|
11
47
|
## Installation
|
|
12
48
|
|
|
@@ -20,65 +56,255 @@ npm install canto-data
|
|
|
20
56
|
import {
|
|
21
57
|
type JournalContent,
|
|
22
58
|
type Page,
|
|
59
|
+
type Attachment,
|
|
23
60
|
SCHEMA_VERSION,
|
|
61
|
+
DEFAULT_JOURNAL_SETTINGS,
|
|
24
62
|
validateJournalContent,
|
|
25
63
|
ValidationError,
|
|
26
64
|
parseManifest,
|
|
27
65
|
migrateIfNeeded,
|
|
28
|
-
} from
|
|
66
|
+
} from "canto-data";
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Validating Journal Data
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { validateJournalContent, ValidationError } from "canto-data";
|
|
29
73
|
|
|
30
|
-
// Validate untrusted journal data
|
|
31
74
|
try {
|
|
32
75
|
const journal = validateJournalContent(untrustedData);
|
|
33
76
|
} catch (err) {
|
|
34
77
|
if (err instanceof ValidationError) {
|
|
35
|
-
console.error(
|
|
78
|
+
console.error(`Field: ${err.field}`);
|
|
79
|
+
console.error(`Expected: ${err.expected}, got: ${err.received}`);
|
|
36
80
|
}
|
|
37
81
|
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Reading an Export Manifest
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { parseManifest } from "canto-data";
|
|
38
88
|
|
|
39
|
-
// Parse an export manifest
|
|
40
89
|
const manifest = parseManifest(manifestJsonString);
|
|
90
|
+
console.log(manifest.encrypted);
|
|
91
|
+
console.log(manifest.journalTitle);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Checking Schema Version and Migrating
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { migrateIfNeeded } from "canto-data";
|
|
41
98
|
|
|
42
|
-
// Migrate data to latest schema
|
|
43
99
|
const result = migrateIfNeeded(rawData, manifest.schemaVersion);
|
|
100
|
+
if (result.migrated) {
|
|
101
|
+
console.log(`Migrated from ${result.fromVersion} to ${result.toVersion}`);
|
|
102
|
+
}
|
|
44
103
|
```
|
|
45
104
|
|
|
46
|
-
|
|
105
|
+
### Working with Exported Journals
|
|
47
106
|
|
|
48
|
-
|
|
107
|
+
A `.canto.zip` file contains:
|
|
49
108
|
|
|
50
|
-
|
|
109
|
+
```text
|
|
110
|
+
{journal-title}.canto.zip
|
|
111
|
+
├── manifest.json
|
|
112
|
+
├── journal.json
|
|
113
|
+
├── settings.json
|
|
114
|
+
├── pages/
|
|
115
|
+
│ ├── {pageId}.json
|
|
116
|
+
│ └── ...
|
|
117
|
+
└── attachments/
|
|
118
|
+
├── {type}-{id}.{ext}
|
|
119
|
+
└── ...
|
|
120
|
+
```
|
|
51
121
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
122
|
+
Example: list all entries from an unencrypted export.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import JSZip from "jszip";
|
|
126
|
+
import { parseManifest } from "canto-data";
|
|
127
|
+
import type { Page } from "canto-data";
|
|
128
|
+
|
|
129
|
+
const zip = await JSZip.loadAsync(zipBuffer);
|
|
130
|
+
const manifest = parseManifest(
|
|
131
|
+
await zip.file("manifest.json")!.async("string"),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (manifest.encrypted) {
|
|
135
|
+
console.log("This export is encrypted and requires the journal password.");
|
|
136
|
+
} else {
|
|
137
|
+
const pageFiles = zip.file(/^pages\/.*\.json$/);
|
|
138
|
+
for (const pf of pageFiles) {
|
|
139
|
+
const page: Page = JSON.parse(await pf.async("string"));
|
|
140
|
+
console.log(`${page.date}: ${page.text.substring(0, 80)}...`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
59
143
|
```
|
|
60
144
|
|
|
61
|
-
##
|
|
145
|
+
## Data Model
|
|
62
146
|
|
|
63
|
-
|
|
147
|
+
```text
|
|
148
|
+
JournalContent
|
|
149
|
+
├── id: string (UUID)
|
|
150
|
+
├── title: string
|
|
151
|
+
├── icon: string (emoji)
|
|
152
|
+
├── date: string (ISO 8601, creation date)
|
|
153
|
+
├── secure: boolean
|
|
154
|
+
├── salt: string (base64, always present)
|
|
155
|
+
├── biometric?: boolean
|
|
156
|
+
├── kdfIterations?: number (PBKDF2, default 50000)
|
|
157
|
+
├── themeOverride?: string
|
|
158
|
+
├── schemaVersion?: string (semver)
|
|
159
|
+
├── version: number (deprecated, always 1)
|
|
160
|
+
├── settings: JournalSettings
|
|
161
|
+
│ ├── use24h: boolean
|
|
162
|
+
│ ├── previewTags: boolean
|
|
163
|
+
│ ├── previewThumbnail: boolean
|
|
164
|
+
│ ├── previewIcons: boolean
|
|
165
|
+
│ ├── filterBar: boolean
|
|
166
|
+
│ ├── sort: 'ascending' | 'descending' | 'none'
|
|
167
|
+
│ ├── autoLocation: boolean
|
|
168
|
+
│ ├── remoteSync: boolean
|
|
169
|
+
│ ├── syncProvider?: 'gdrive'
|
|
170
|
+
│ ├── autoSync: boolean
|
|
171
|
+
│ └── themeOverride?: string
|
|
172
|
+
└── pages: Page[]
|
|
173
|
+
├── id: string (UUID)
|
|
174
|
+
├── text: string (Markdown)
|
|
175
|
+
├── date: string (ISO 8601, entry date)
|
|
176
|
+
├── modified: number (Unix timestamp ms)
|
|
177
|
+
├── deleted: boolean
|
|
178
|
+
├── thumbnail?: string (base64)
|
|
179
|
+
├── tags: string[]
|
|
180
|
+
├── location?: GeoLocation
|
|
181
|
+
│ ├── latitude: number
|
|
182
|
+
│ ├── longitude: number
|
|
183
|
+
│ ├── altitude?: number
|
|
184
|
+
│ └── accuracy?: number
|
|
185
|
+
├── comments: Comment[]
|
|
186
|
+
│ ├── id: string
|
|
187
|
+
│ ├── text: string
|
|
188
|
+
│ └── date: string (ISO 8601)
|
|
189
|
+
├── images: Attachment[]
|
|
190
|
+
│ ├── id: string (UUID)
|
|
191
|
+
│ ├── path: string
|
|
192
|
+
│ ├── name: string (original filename)
|
|
193
|
+
│ ├── type: 'image'
|
|
194
|
+
│ ├── encrypted: boolean
|
|
195
|
+
│ ├── size?: number (bytes)
|
|
196
|
+
│ └── deleted: boolean
|
|
197
|
+
└── files: Attachment[]
|
|
198
|
+
└── same fields as images, with type: 'file'
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Schema Versioning
|
|
202
|
+
|
|
203
|
+
Canto journal schemas follow [semver](https://semver.org/):
|
|
204
|
+
|
|
205
|
+
| Change type | Version bump | Migration needed? |
|
|
206
|
+
| -------------------------------------- | ------------ | ----------------- |
|
|
207
|
+
| Breaking (field removed, type changed) | MAJOR | Yes |
|
|
208
|
+
| New optional field | MINOR | No |
|
|
209
|
+
| Documentation or validation fix | PATCH | No |
|
|
210
|
+
|
|
211
|
+
The schema version is stored in `JournalContent.schemaVersion` and `ExportManifest.schemaVersion`. Legacy data without `schemaVersion` is treated as `0.16.0`. Migrations are forward-only.
|
|
212
|
+
|
|
213
|
+
### Migration History
|
|
214
|
+
|
|
215
|
+
| From | To | Description |
|
|
216
|
+
| ------ | ------ | --------------------------------------------------- |
|
|
217
|
+
| 0.16.0 | 0.17.0 | Remove deprecated `showMarkdownPlaceholder` setting |
|
|
218
|
+
|
|
219
|
+
## Export Format Details
|
|
220
|
+
|
|
221
|
+
### `manifest.json`
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
{
|
|
225
|
+
"version": 1,
|
|
226
|
+
"schemaVersion": "0.17.0",
|
|
227
|
+
"appVersion": "0.17.0",
|
|
228
|
+
"exportDate": "2026-01-01T00:00:00.000Z",
|
|
229
|
+
"encrypted": false,
|
|
230
|
+
"journalTitle": "My Journal",
|
|
231
|
+
"salt": "base64...",
|
|
232
|
+
"kdfIterations": 50000
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
- `version`: Manifest format version, always `1`
|
|
237
|
+
- `schemaVersion`: Journal schema version; absent in legacy exports and treated as `0.16.0`
|
|
238
|
+
- `encrypted`: If `true`, all JSON and attachment content is AES-256-GCM encrypted
|
|
239
|
+
- `salt` and `kdfIterations`: Present for password-protected journals
|
|
240
|
+
|
|
241
|
+
### Encrypted Exports
|
|
242
|
+
|
|
243
|
+
When `encrypted: true`, decryption requires the journal password. The ciphertext format is `[12-byte nonce][ciphertext][16-byte GCM tag]` using AES-256-GCM. See [Canto SECURITY.md](https://github.com/pboueke/canto/blob/main/SECURITY.md) for the full encryption model.
|
|
244
|
+
|
|
245
|
+
### Import Behavior
|
|
246
|
+
|
|
247
|
+
Importing always creates a new journal with new UUIDs, so re-importing the same archive is safe. Shared attachments get individual copies per page.
|
|
64
248
|
|
|
65
|
-
|
|
249
|
+
## Filesystem Structure
|
|
66
250
|
|
|
67
|
-
|
|
68
|
-
2. **npm token**: Generate an automation token at npmjs.com > Access Tokens > Generate New Token (Automation)
|
|
69
|
-
3. **GitHub secret**: Add the token as `NPM_TOKEN` in the repo's Settings > Secrets and variables > Actions
|
|
70
|
-
4. **First publish**: Run `npm run build && npm publish` after `npm login`, or push v1.0.0 to main
|
|
71
|
-
5. **Subsequent publishes**: Bump version in `package.json`, merge to `main` — GitHub Actions auto-publishes
|
|
251
|
+
### Native (Android and iOS)
|
|
72
252
|
|
|
73
|
-
|
|
253
|
+
```text
|
|
254
|
+
{documentDirectory}/canto/
|
|
255
|
+
├── journals.json
|
|
256
|
+
├── {journalId}/
|
|
257
|
+
│ ├── metadata.json
|
|
258
|
+
│ ├── pages/
|
|
259
|
+
│ │ └── {pageId}.json
|
|
260
|
+
│ └── attachments/
|
|
261
|
+
│ └── [e]{img|fl}-{pageId}-{hash}.{ext}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Attachment naming uses `{encPrefix}{typePrefix}-{pageId}-{hash}.{ext}` where `e` means password-encrypted and `img` or `fl` indicates the attachment type.
|
|
265
|
+
|
|
266
|
+
### Web (IndexedDB)
|
|
267
|
+
|
|
268
|
+
```text
|
|
269
|
+
Database: 'canto' (version 1), Object store: 'files' (keyPath: 'path')
|
|
270
|
+
|
|
271
|
+
Virtual paths mirror native layout:
|
|
272
|
+
canto/journals.json
|
|
273
|
+
canto/{journalId}/metadata.json
|
|
274
|
+
canto/{journalId}/pages/{pageId}.json
|
|
275
|
+
canto/{journalId}/attachments/{typePrefix}-{pageId}-{hash}.{ext}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Google Drive
|
|
279
|
+
|
|
280
|
+
All journal content on Google Drive is AES-256-GCM encrypted before upload. Only the registry and sync index are stored unencrypted.
|
|
281
|
+
|
|
282
|
+
```text
|
|
283
|
+
My Drive/Canto/
|
|
284
|
+
├── {journalId}/
|
|
285
|
+
│ ├── meta.json
|
|
286
|
+
│ ├── index.json
|
|
287
|
+
│ ├── pages/{pageId}.json
|
|
288
|
+
│ └── attachments/{filename}
|
|
289
|
+
App Data (hidden):
|
|
290
|
+
└── canto-journals.json
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Development
|
|
74
294
|
|
|
75
295
|
```bash
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
npm
|
|
79
|
-
|
|
296
|
+
git clone https://github.com/pboueke/canto-data.git
|
|
297
|
+
cd canto-data
|
|
298
|
+
npm install
|
|
299
|
+
npm test
|
|
300
|
+
npm run test:ci
|
|
301
|
+
npm run build
|
|
80
302
|
```
|
|
81
303
|
|
|
304
|
+
The repository requires `100%` test coverage. Local hooks keep the README version, test count, and coverage badges in sync with the current test suite.
|
|
305
|
+
|
|
306
|
+
Release versioning is derived from the top entry in [CHANGELOG.md](CHANGELOG.md). The pre-commit hook syncs `package.json` and the README version badge from that changelog entry automatically.
|
|
307
|
+
|
|
82
308
|
## License
|
|
83
309
|
|
|
84
|
-
MIT
|
|
310
|
+
MIT. See [LICENSE](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canto-data",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Canto journal data model — types, validation, versioning, and format utilities",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -40,7 +40,11 @@
|
|
|
40
40
|
"test": "jest",
|
|
41
41
|
"test:coverage": "jest --coverage",
|
|
42
42
|
"test:ci": "jest --coverage --ci",
|
|
43
|
-
"
|
|
43
|
+
"verify:repo": "sh ./scripts/verify-repo.sh",
|
|
44
|
+
"format": "prettier --write \"**/*.{ts,json,md}\"",
|
|
45
|
+
"format:check": "prettier --check \"**/*.{ts,json,md}\"",
|
|
46
|
+
"prepare": "husky",
|
|
47
|
+
"prepublishOnly": "npm run build && npm run verify:repo"
|
|
44
48
|
},
|
|
45
49
|
"keywords": [
|
|
46
50
|
"canto",
|
|
@@ -58,8 +62,16 @@
|
|
|
58
62
|
},
|
|
59
63
|
"devDependencies": {
|
|
60
64
|
"@types/jest": "^30.0.0",
|
|
65
|
+
"husky": "^9.1.7",
|
|
61
66
|
"jest": "^30.3.0",
|
|
67
|
+
"lint-staged": "^16.3.3",
|
|
68
|
+
"prettier": "^3.8.1",
|
|
62
69
|
"ts-jest": "^29.4.6",
|
|
63
70
|
"typescript": "^5.9.3"
|
|
71
|
+
},
|
|
72
|
+
"lint-staged": {
|
|
73
|
+
"*.{ts,json,md}": [
|
|
74
|
+
"prettier --write"
|
|
75
|
+
]
|
|
64
76
|
}
|
|
65
77
|
}
|