# Open Print

Open Print is the portable record and verification layer for digital art
editions. It defines signed JSON documents for artworks, editions, issuers,
certificates, issuance events, registries, and checkpoints.

The project is intentionally narrower than an application. It does not provide
accounts, payments, a marketplace, object storage, or a hosting service. The
implemented reference application is [Open Press](https://open-press.xyz/), which uses
this package to run institutional issuance, claims, collection, and public
verification workflows.

## Current implementation

The v1 implementation includes:

- JSON Schemas and runtime validation for issuer, certificate, registry-event,
  checkpoint, and archive documents;
- deterministic JSON canonicalization and SHA-256 digests;
- Ed25519 issuer keys, signed certificates, and signed registry events;
- unique certificate IDs and edition sequences;
- append-only issuance, cancellation, and replacement semantics;
- signed registry checkpoints for anchoring registry history;
- an atomic Node file-store for single-machine issuance;
- portable archive and restore commands;
- content-addressed static artwork publishing;
- verification reports that separate integrity, issuer, signature, registry, and
  current-status findings;
- a v1 compatibility consumer, frozen public-surface baseline, and independent
  Python conformance fixture;
- deterministic security, tamper, malformed-input, and release checks.

Open Print `1.0.0` is published to npm under the `latest` dist-tag. The older
`0.1` API remains available so applications can continue reading earlier
certificates while adopting the signed v1 format.

## Open Press integration

Open Press demonstrates how the core package fits inside a hosted application:

```mermaid
flowchart TD
    media[Institution-hosted media]
    version[Open Press artwork version]
    edition[Fixed edition]
    certificate[Open Print certificate]
    event[Signed registry event]

    media -->|SHA-256 identity| version
    version --> edition
    edition --> certificate
    certificate --> event
```

The integration boundary is split deliberately:

1. Open Press hashes institution-hosted media and stores the media identity with
   an immutable artwork version. Open Press does not retain the original file.
2. A server-side managed signer provisions the Open Print issuer document and
   key, then uses `signV1Certificate` and `appendV1Issuance` to create the
   certificate and issuance event.
3. Supabase stores the public issuer, certificate, registry-event, and
   checkpoint documents. Database constraints enforce the application’s fixed
   edition supply and unique `edition_id + sequence` allocation.
4. Collector claims, transfers, display permissions, generative output
   records, and current-holder projections are application records around the
   signed certificate. They do not mutate the certificate or registry event.

The Open Press signer uses the core API directly:

```ts
import {
  OPEN_PRINT_V1_ISSUER_SCHEMA,
  OPEN_PRINT_V1_VERSION,
  appendV1Issuance,
  canonicalize,
  exportV1IssuerPublicKey,
  generateV1IssuerKeyPair,
  sha256Hex,
  signV1Certificate,
} from "@open-print/core";
```

The private key stays in signer infrastructure. It is never sent to the React
client, collector browser, or public registry. Open Press currently uses a
file-backed signer for local development and a pilot cPanel deployment; a
production deployment should replace that key store with an HSM/KMS-backed
provider.

## Install

```bash
npm install @open-print/core
```

The current `1.0.0` release is available on npm under the `latest` dist-tag.
For local development, clone this repository and install it from a local path
when you need to work against unreleased changes. Open Press currently uses the
local dependency path `../smartcontract/open-print`; its standalone signer
deployment vendors the package as a tarball.

The package uses Ajv to apply its published JSON Schemas at runtime. It supports
modern browsers and Node.js 20.10 or newer.

## A v1 certificate workflow

An issuer first creates or obtains a v1 issuer record and a protected Ed25519
private key. The application then creates claims, signs the certificate, and
appends the corresponding issuance event:

```ts
import {
  appendV1Issuance,
  signV1Certificate,
  verifyV1Certificate,
} from "@open-print/core";

const certificate = await signV1Certificate(claims, privateKey, {
  keyId: "issuer:gallery-example#2026-01",
});

const registry = await appendV1Issuance(certificate, events, {
  id: "event:gallery-example:certificate-001",
  signing: { issuer, privateKey, keyId: "issuer:gallery-example#2026-01" },
});

const report = await verifyV1Certificate(certificate, {
  issuer,
  registry,
});
```

The verification report distinguishes record integrity, issuer identity,
signature validity, registry presence, and current status. A missing issuer
record is not silently collapsed into a binary “authentic” or “inauthentic”
answer.

## Media identity and immutable records

Open Print hashes bytes; it does not host them. An application can identify an
institution-hosted file before publishing an artwork version:

```ts
import { sha256Hex } from "@open-print/core";

const digest = await sha256Hex(await file.arrayBuffer());
```

Open Press stores that digest, source URL, media type, and byte size as part of
an immutable artwork version. If the source bytes change, the digest changes
and the application must publish a new version. The core package does not
assume that a URL, a filename, or a database row is an identity.

An issued certificate is frozen by the library and identified by the SHA-256
digest of its complete claims. Editing a downloaded JSON file is possible, but
the edit produces a different digest and breaks the issuer signature.

`appendV1Issuance` returns a new registry with one event and leaves the prior
registry untouched. Cancellation and replacement are later events that point
back to the earlier certificate.

## Collector privacy

A certificate may contain a public display name or no collector name at all.
Private labels belong in application data and never in signed claims.
`prepareV1Collector` keeps those destinations separate:

```ts
import { prepareV1Collector } from "@open-print/core";

const collector = prepareV1Collector({
  publicDisplayName: null,
  privateLabel: "local conservation note",
});

claims.collector = collector.claims;
```

Removing `collector.privateLabel` later does not change the certificate. The
full policy and verification limits are documented in
[docs/PRIVACY_AND_CLAIMS.md](docs/PRIVACY_AND_CLAIMS.md).

## Registry checkpoints

A checkpoint is a signed note containing a registry length and the digest of
the last event at that point. A verifier that remembers one checkpoint can
detect a shorter registry or a different checkpoint branch later.

```ts
const checkpoint = await createV1RegistryCheckpoint(registry, {
  id: "checkpoint:gallery-example:2026-07-29",
  signing,
  previousCheckpoint,
});
```

Check a saved checkpoint and registry from the command line:

```bash
npx open-print-verify-checkpoint \
  checkpoint.json issuer.json registry.json previous-checkpoint.json
```

Checkpoints are ordinary signed JSON. They can live in Git, on removable media,
or in an application archive.

## Local registry and archives

The Node-only file store coordinates concurrent writers with a lock and
replaces its archive atomically. Requests arriving together are committed as
one batch, while each certificate receives its own sequence and signed
issuance event.

```ts
import { V1FileRegistryStore } from "@open-print/core/node/file-store";

const store = new V1FileRegistryStore("./data/open-print-archive.json");
const result = await store.issue({
  certificateId,
  editionId,
  eventId,
  signing,
  createClaims: ({ sequence, issuedAt }) => ({
    ...claims,
    id: certificateId,
    issuedAt,
    sequence,
  }),
});
```

This reference store is intended for one machine and an ordinary local
filesystem. A multi-server application such as Open Press must provide the
same transaction boundary with its database; the library does not provide a
distributed database lock.

An archive contains public issuer records, certificates, registry events, and
checkpoints in one integrity-checked JSON document. Private keys are always
excluded:

```bash
open-print-archive verify ./open-print-archive.json
open-print-archive restore ./open-print-archive.json ./restored-archive
```

Restore refuses to overwrite an existing directory.

## Static artwork publishing

The `open-print-publish` command copies a file into a path containing the
complete SHA-256 digest:

```text
/artworks/{work-id}/{sha256}/{filename}
```

```bash
npx open-print-publish \
  ./artworks/residual-field-07/index.html \
  residual-field-07 \
  ./public
```

If one byte changes, the output path changes. The generated directory can be
committed to Git or deployed with an ordinary static host.

## What belongs here

Open Print owns:

- artwork, edition, issuer, certificate, event, checkpoint, and archive
  document formats;
- published JSON Schemas and runtime validation;
- deterministic serialization, SHA-256 hashing, and integrity checks;
- Ed25519 signature construction and verification;
- certificate IDs, edition sequences, and append-only registry rules;
- content-addressed release paths;
- compatibility and conformance rules that other implementations can test.

An application built with Open Print is responsible for:

- user, institution, and collector accounts;
- storing and delivering artwork files;
- payment and identity-provider integrations;
- claim links, website embeds, generative collection, and exhibition views;
- database transactions and current-holder projections;
- protecting signing keys and operating a signer service;
- publishing issuer, correction, cancellation, and lifecycle records.

## What “non-fungible” means here

Every certificate has its own ID, sequence number within an edition, and
content fingerprint. The v1 registry rejects a reused certificate ID and
rejects two certificates claiming the same edition sequence.

That makes two certificates non-interchangeable as issuance records. It does
not create a token, prevent copying the artwork, establish legal ownership, or
create a financial asset.

## Firm boundary

Open Print does not use blockchains, cryptocurrency, wallets, tokens, NFTs, or
distributed ledgers. SHA-256 is used only to check whether two sets of bytes or
records match. A hash is not a token, proof of legal ownership, or proof of
authorship.

## Working on the project

```bash
npm install
npm test
npm run compatibility:check
npm run test:security
npm run audit:production
npm run check
```

`npm run check` runs the tests, compiles an external TypeScript consumer,
checks the frozen public contract, and inspects the package that would be
published. Development checks require Python 3.9 or newer for the independent
conformance implementation; the published library itself does not depend on
Python.

`npm run test:security` runs the deterministic adversarial suite for signature
metadata rewriting, digest recomputation, registry and checkpoint tampering,
canonical encodings, malformed JSON, and CLI input limits.

`npm run release:verify` performs two isolated builds and checks that their
shipped files and package tarballs are identical. `npm run release:artifacts`
creates an ignored local directory containing the tarball, CycloneDX SBOM,
release manifest, and checksums. Neither command publishes anything.

See [CONTRIBUTING.md](CONTRIBUTING.md) before changing a record shape. The
stability contract is in [docs/V1.md](docs/V1.md), the compatibility procedure
is in [docs/COMPATIBILITY_POLICY.md](docs/COMPATIBILITY_POLICY.md), and the
release gates are in [docs/RC1_CHECKLIST.md](docs/RC1_CHECKLIST.md).
Validation, key management, checkpoints, archives, privacy, threat modeling,
and releasing are documented in the corresponding files under `docs/`.
The independent implementation is documented in
[conformance/python/README.md](conformance/python/README.md). Report
vulnerabilities according to [SECURITY.md](SECURITY.md).
