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.
Files changed (2) hide show
  1. package/README.md +259 -33
  2. package/package.json +14 -2
package/README.md CHANGED
@@ -1,12 +1,48 @@
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)
5
- [![CI](https://github.com/pboueke/canto-data/actions/workflows/ci.yml/badge.svg)](https://github.com/pboueke/canto-data/actions/workflows/ci.yml)
6
+ ![Version](https://img.shields.io/badge/version-1.0.2-green)
7
+ ![Tests](https://img.shields.io/badge/tests-156%2F156%20passed-brightgreen)
8
+ ![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)
9
+ [![CI Main](https://github.com/pboueke/canto-data/actions/workflows/ci.yml/badge.svg?branch=main&event=push)](https://github.com/pboueke/canto-data/actions/workflows/ci.yml)
10
+ [![CI PR](https://github.com/pboueke/canto-data/actions/workflows/ci.yml/badge.svg?event=pull_request)](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
- 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.
38
+ What it does **not** include:
8
39
 
9
- **MIT-licensed** with **zero dependencies** — use it to build tools that read, validate, or manipulate Canto journals without copyleft obligations.
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 'canto-data';
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(`${err.field}: expected ${err.expected}, got ${err.received}`);
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
- ## Documentation
105
+ ### Working with Exported Journals
47
106
 
48
- See **[DATA.md](DATA.md)** for the full data model reference, export format specification, filesystem structure, and usage examples.
107
+ A `.canto.zip` file contains:
49
108
 
50
- ## Development
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
- ```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/
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
- ## Publishing
145
+ ## Data Model
62
146
 
63
- See the [npm Publishing Setup](#npm-publishing-setup) section below.
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
- ### npm Publishing Setup
249
+ ## Filesystem Structure
66
250
 
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
251
+ ### Native (Android and iOS)
72
252
 
73
- ### Version bump
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
- 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
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 see [LICENSE](LICENSE).
310
+ 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.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
- "prepublishOnly": "npm run build && npm test"
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
  }