npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

canto-data

v1.0.4

Published

Canto journal data model — types, validation, versioning, and format utilities

Readme

canto-data

Data model library for Canto, a private encrypted journaling app.

License: MIT Version Tests Coverage

canto-data provides TypeScript types, runtime validation, schema versioning, migration infrastructure, and export format utilities for Canto journals.

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.

Relationship to the Canto App

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.

canto-data (MIT)
└── src/
    ├── types.ts              # All TypeScript interfaces
    ├── validation.ts         # Type guards and structural validators
    ├── version.ts            # Schema version constant and semver utils
    ├── migration.ts          # Forward-only migration runner
    ├── migrations/           # Migration registry
    └── format.ts             # Export manifest and ZIP format utilities

What canto-data owns:

  • All journal data types (Journal, Page, Attachment, Comment, and related structures)
  • Runtime validation and type guards
  • Schema versioning and migration framework
  • Export format specification (manifest structure and attachment naming)

What it does not include:

  • Encryption and decryption
  • Storage backends
  • Sync integrations
  • UI components

Those pieces live in the Canto app.

Installation

npm install canto-data

Quick Start

import {
  type JournalContent,
  type Page,
  type Attachment,
  SCHEMA_VERSION,
  DEFAULT_JOURNAL_SETTINGS,
  validateJournalContent,
  ValidationError,
  parseManifest,
  migrateIfNeeded,
} from "canto-data";

Validating Journal Data

import { validateJournalContent, ValidationError } from "canto-data";

try {
  const journal = validateJournalContent(untrustedData);
} catch (err) {
  if (err instanceof ValidationError) {
    console.error(`Field: ${err.field}`);
    console.error(`Expected: ${err.expected}, got: ${err.received}`);
  }
}

Reading an Export Manifest

import { parseManifest } from "canto-data";

const manifest = parseManifest(manifestJsonString);
console.log(manifest.encrypted);
console.log(manifest.journalTitle);

Checking Schema Version and Migrating

import { migrateIfNeeded } from "canto-data";

const result = migrateIfNeeded(rawData, manifest.schemaVersion);
if (result.migrated) {
  console.log(`Migrated from ${result.fromVersion} to ${result.toVersion}`);
}

Working with Exported Journals

A .canto.zip file contains:

{journal-title}.canto.zip
├── manifest.json
├── journal.json
├── settings.json
├── pages/
│   ├── {pageId}.json
│   └── ...
└── attachments/
    ├── {type}-{id}.{ext}
    └── ...

Example: list all entries from an unencrypted export.

import JSZip from "jszip";
import { parseManifest } from "canto-data";
import type { Page } from "canto-data";

const zip = await JSZip.loadAsync(zipBuffer);
const manifest = parseManifest(
  await zip.file("manifest.json")!.async("string"),
);

if (manifest.encrypted) {
  console.log("This export is encrypted and requires the journal password.");
} else {
  const pageFiles = zip.file(/^pages\/.*\.json$/);
  for (const pf of pageFiles) {
    const page: Page = JSON.parse(await pf.async("string"));
    console.log(`${page.date}: ${page.text.substring(0, 80)}...`);
  }
}

Data Model

JournalContent
├── id: string (UUID)
├── title: string
├── icon: string (emoji)
├── date: string (ISO 8601, creation date)
├── secure: boolean
├── salt: string (base64, always present)
├── biometric?: boolean
├── kdfIterations?: number (PBKDF2, default 50000)
├── themeOverride?: string
├── schemaVersion?: string (semver)
├── version: number (deprecated, always 1)
├── settings: JournalSettings
│   ├── use24h: boolean
│   ├── previewTags: boolean
│   ├── previewThumbnail: boolean
│   ├── previewIcons: boolean
│   ├── filterBar: boolean
│   ├── sort: 'ascending' | 'descending' | 'none'
│   ├── autoLocation: boolean
│   ├── remoteSync: boolean
│   ├── syncProvider?: 'gdrive'
│   ├── autoSync: boolean
│   └── themeOverride?: string
└── pages: Page[]
    ├── id: string (UUID)
    ├── text: string (Markdown)
    ├── date: string (ISO 8601, entry date)
    ├── modified: number (Unix timestamp ms)
    ├── deleted: boolean
    ├── thumbnail?: string (base64)
    ├── tags: string[]
    ├── location?: GeoLocation
    │   ├── latitude: number
    │   ├── longitude: number
    │   ├── altitude?: number
    │   └── accuracy?: number
    ├── comments: Comment[]
    │   ├── id: string
    │   ├── text: string
    │   └── date: string (ISO 8601)
    ├── images: Attachment[]
    │   ├── id: string (UUID)
    │   ├── path: string
    │   ├── name: string (original filename)
    │   ├── type: 'image'
    │   ├── encrypted: boolean
    │   ├── size?: number (bytes)
    │   └── deleted: boolean
    └── files: Attachment[]
        └── same fields as images, with type: 'file'

Schema Versioning

Canto journal schemas follow semver:

| Change type | Version bump | Migration needed? | | -------------------------------------- | ------------ | ----------------- | | Breaking (field removed, type changed) | MAJOR | Yes | | New optional field | MINOR | No | | Documentation or validation fix | PATCH | No |

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.

Migration History

| From | To | Description | | ------ | ------ | --------------------------------------------------- | | 0.16.0 | 0.17.0 | Remove deprecated showMarkdownPlaceholder setting |

Export Format Details

manifest.json

{
  "version": 1,
  "schemaVersion": "0.17.0",
  "appVersion": "0.17.0",
  "exportDate": "2026-01-01T00:00:00.000Z",
  "encrypted": false,
  "journalTitle": "My Journal",
  "salt": "base64...",
  "kdfIterations": 50000
}
  • version: Manifest format version, always 1
  • schemaVersion: Journal schema version; absent in legacy exports and treated as 0.16.0
  • encrypted: If true, all JSON and attachment content is AES-256-GCM encrypted
  • salt and kdfIterations: Present for password-protected journals

Encrypted Exports

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 for the full encryption model.

Import Behavior

Importing always creates a new journal with new UUIDs, so re-importing the same archive is safe. Shared attachments get individual copies per page.

Filesystem Structure

Native (Android and iOS)

{documentDirectory}/canto/
├── journals.json
├── {journalId}/
│   ├── metadata.json
│   ├── pages/
│   │   └── {pageId}.json
│   └── attachments/
│       └── [e]{img|fl}-{pageId}-{hash}.{ext}

Attachment naming uses {encPrefix}{typePrefix}-{pageId}-{hash}.{ext} where e means password-encrypted and img or fl indicates the attachment type.

Web (IndexedDB)

Database: 'canto' (version 1), Object store: 'files' (keyPath: 'path')

Virtual paths mirror native layout:
  canto/journals.json
  canto/{journalId}/metadata.json
  canto/{journalId}/pages/{pageId}.json
  canto/{journalId}/attachments/{typePrefix}-{pageId}-{hash}.{ext}

Google Drive

All journal content on Google Drive is AES-256-GCM encrypted before upload. Only the registry and sync index are stored unencrypted.

My Drive/Canto/
├── {journalId}/
│   ├── meta.json
│   ├── index.json
│   ├── pages/{pageId}.json
│   └── attachments/{filename}
App Data (hidden):
└── canto-journals.json

Development

git clone https://github.com/pboueke/canto-data.git
cd canto-data
npm install
npm test
npm run test:ci
npm run build

The repository requires 100% test coverage. Local hooks keep the README version, test count, and coverage badges in sync with the current test suite.

Release versioning is derived from the top entry in CHANGELOG.md. The pre-commit hook syncs package.json and the README version badge from that changelog entry automatically.

License

MIT. See LICENSE.