feat!: neutral built-in templates; private templates per tenant #27

Merged
jlxq0 merged 5 commits from fix/t_f3a1cca2-audit into main 2026-10-24 04:44:09 +00:00
Owner

The public repository and image now carry only neutral example templates; branded and personal templates move to a private repository and are served per tenant.

What changes

  • Three neutral built-ins, in IBM Plex, each with a JSON Schema and a working fixture:
    • letter: a business letter with a window-envelope address block;
    • documentation: title page, contents, numbered sections, code blocks, callouts and tables;
    • invoice: a data template with line items, tax grouped by rate, and totals in exact decimal arithmetic. Each line is rounded to cents before summing, so the printed rows always add up.
  • Removed:
    • the four branded templates;
    • their static font copies;
    • the OfficeMaster sync script and its CI drift check;
    • the upload fixture's photographs, whose licences were unconfirmed. Generated images replace them.
  • Private templates, per tenant. A deployment can mount a second directory with TYPST_MCP_PRIVATE_TEMPLATE_DIR, which only the tenants in TYPST_MCP_PRIVATE_TEMPLATE_TENANTS can use.
    • Every other authenticated caller gets a byte-for-byte identical answer to a missing template. This covers list, schema, render, overrides, delete and the available list, over both REST and MCP.
    • Anonymous callers get 401.
    • Only the owning tenant can fetch a private render or mint a signed link to it.
    • Metrics and audit logs record private, never the name.
    • Startup refuses: an empty or missing private directory, a name that is both public and private, and either setting without the other.
  • Access log: it now records the matched route (/api/v1/templates/{name}) instead of the raw path, so a private template's name never reaches the logs. Paths no route matches log as <unmatched>.
  • Smoke: a new step asserts that the smoke key sees exactly the three public templates.

BREAKING

hanso, ksc, lenno and freudenberg are no longer built in. Earlier history and the tags up to v0.2.1 are left unchanged.

Verification

  • cargo fmt --check, cargo clippy -D warnings and cargo test --all-features --locked all pass: 211 unit tests plus the http, mcp, sandbox and templates suites. Fonts are verified (68 files).
  • Each new privacy, log, invoice and catalog test was shown to fail when the behaviour it guards is removed.
  • An independent session audited the publication and reviewed the code three times. Every round passed. The findings from round 1 are fixed and were confirmed closed.
  • The audit confirmed four points:
    • no brand artwork, embedded SVG path data, book or personal content in any commit or in the tree;
    • none of these commits is reachable from any branch that carries logos;
    • the only new binaries are five generated colour-field JPEGs.
  • letter, rendered through a real MCP client on the production renderer, is byte-identical to the local render.

Deployment of the private template mount is tracked separately: credentials and rollout are handled on the operations card.

🤖 Generated with Claude Code

https://claude.ai/code/session_014Tow3aw74AFPQwy1Smuqdo

The public repository and image now carry only neutral example templates; branded and personal templates move to a private repository and are served per tenant. ## What changes - **Three neutral built-ins**, in IBM Plex, each with a JSON Schema and a working fixture: - `letter`: a business letter with a window-envelope address block; - `documentation`: title page, contents, numbered sections, code blocks, callouts and tables; - `invoice`: a data template with line items, tax grouped by rate, and totals in exact decimal arithmetic. Each line is rounded to cents before summing, so the printed rows always add up. - **Removed:** - the four branded templates; - their static font copies; - the OfficeMaster sync script and its CI drift check; - the upload fixture's photographs, whose licences were unconfirmed. Generated images replace them. - **Private templates, per tenant.** A deployment can mount a second directory with `TYPST_MCP_PRIVATE_TEMPLATE_DIR`, which only the tenants in `TYPST_MCP_PRIVATE_TEMPLATE_TENANTS` can use. - Every other authenticated caller gets a byte-for-byte identical answer to a missing template. This covers list, schema, render, overrides, delete and the `available` list, over both REST and MCP. - Anonymous callers get 401. - Only the owning tenant can fetch a private render or mint a signed link to it. - Metrics and audit logs record `private`, never the name. - Startup refuses: an empty or missing private directory, a name that is both public and private, and either setting without the other. - **Access log:** it now records the matched route (`/api/v1/templates/{name}`) instead of the raw path, so a private template's name never reaches the logs. Paths no route matches log as `<unmatched>`. - **Smoke:** a new step asserts that the smoke key sees exactly the three public templates. ## BREAKING `hanso`, `ksc`, `lenno` and `freudenberg` are no longer built in. Earlier history and the tags up to v0.2.1 are left unchanged. ## Verification - `cargo fmt --check`, `cargo clippy -D warnings` and `cargo test --all-features --locked` all pass: 211 unit tests plus the http, mcp, sandbox and templates suites. Fonts are verified (68 files). - Each new privacy, log, invoice and catalog test was shown to fail when the behaviour it guards is removed. - An independent session audited the publication and reviewed the code three times. Every round passed. The findings from round 1 are fixed and were confirmed closed. - The audit confirmed four points: - no brand artwork, embedded SVG path data, book or personal content in any commit or in the tree; - none of these commits is reachable from any branch that carries logos; - the only new binaries are five generated colour-field JPEGs. - `letter`, rendered through a real MCP client on the production renderer, is byte-identical to the local render. Deployment of the private template mount is tracked separately: credentials and rollout are handled on the operations card. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_014Tow3aw74AFPQwy1Smuqdo
The public image now carries three neutral, polished examples — `letter`,
`documentation` and `invoice` (a data template with exact decimal totals) —
in IBM Plex, each with a schema and a working fixture. The branded templates,
their static fonts, the cross-repository sync script and its CI drift check
leave this repository; they live in a private repository.

A deployment can mount a second template directory
(TYPST_MCP_PRIVATE_TEMPLATE_DIR) whose templates exist only for the tenants in
TYPST_MCP_PRIVATE_TEMPLATE_TENANTS. Everyone else gets exactly the answer a
missing name gets, from list, schema, render, delete and the "available" list.
Metrics and audit record a private template only as "private". Startup refuses
an empty or missing private directory, a name in both sets, and either setting
without the other. Health and the startup log count templates, never name them.

The upload test fixture's photographs, copied from a branded example with
unconfirmed licences, are replaced by generated images.

BREAKING CHANGE: the `hanso`, `ksc`, `lenno` and `freudenberg` templates are no
longer built in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Tow3aw74AFPQwy1Smuqdo
- invoice: each line amount is rounded to cents before it is summed, so the
  printed rows always add up to the printed subtotal; the totals are a pure
  `invoice-totals` function the template prints from and the tests call.
- A request for a template literally named "private" is labelled "other", so
  it cannot be counted as a private-template render.
- Tests: Bob's REST answers for a private name and for a missing name are
  byte-identical once the name is masked (schema, render, delete); the same
  over MCP (list, schema, render); metrics show "private" and never the name;
  the invoice fixture's tax groups and total are asserted, and a rows-sum
  case fails if per-line rounding is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Tow3aw74AFPQwy1Smuqdo
The access log recorded the raw request path, so a request for
/api/v1/templates/<name> wrote a private template's name into the log, for its
own tenant as well as anyone probing. It now records the matched route pattern
(`/api/v1/templates/{name}`), and `<unmatched>` for a path no route claims; a
unit test captures the log and fails if a template name reaches it.

The private-template test now also covers render overrides naming a private
template, anonymous schema and render (401), the rendered document (the owner's
key reads it; another tenant's key and no key do not) and signed links (the
owner can mint one; another tenant cannot).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Tow3aw74AFPQwy1Smuqdo
test: positive control for private-template overrides
Some checks failed
CI / cargo (pull_request) Failing after 1m49s
CI / docker (pull_request) Has been skipped
ff711d79c0
Alice can override a file of her private template, so Bob's answer for the
same request, identical to the one for a missing template, is a real refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Tow3aw74AFPQwy1Smuqdo
Merge remote-tracking branch 'origin/main' into fix/t_f3a1cca2-audit
Some checks failed
CI / cargo (pull_request) Successful in 6m15s
CI / docker (pull_request) Failing after 8s
36dae11810
jlxq0 merged commit c5e8b714b6 into main 2026-10-24 04:44:09 +00:00
jlxq0 referenced this pull request from a commit 2026-10-24 04:44:49 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jlxq0/typst-mcp!27
No description provided.