canto-data 1.0.0 → 1.0.1

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