> ## Documentation Index
> Fetch the complete documentation index at: https://llmwiki.atomicstrata.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Publish a Signed Template Tap

> Package, sign, host, and verify declarative llmwiki profile templates.

This guide is for template publishers and tap operators. It shows how to turn a
validated declarative profile package into an immutable signed release that an
llmwiki user can discover, verify, install, and update.

llmwiki ships the publisher workflow: `template publish init | add | build | rotate |
revoke | verify` creates keys, signs packages, builds a verified static tree, and
manages key rotation and revocation. It does not upload: publishing the built tree to
HTTPS hosting stays with your existing deploy tooling.

Everything below runs offline. Private keys never leave your workspace, and the protocol
appendix at the end remains the interoperability contract for anyone implementing it
independently.

<Warning>
  A signature proves which key signed exact bytes. It does not certify that a
  profile is useful or that its labels are safe instructions. Remote profiles
  remain untrusted declarative input and pass the ordinary profile validator on
  every install or update.
</Warning>

## What you publish

A tap is a static HTTPS directory containing one signed index and immutable,
content-addressed package envelopes:

```text theme={null}
https://templates.example/community/index.json
https://templates.example/community/packages/sha256/<payload-sha256>.json
```

If `index.json` is at `/community/index.json`, llmwiki derives each package URL
as `/community/packages/sha256/<hex>.json`. The package must stay on the exact
scheme, host, and port used by the index. Redirects to another origin are
refused.

Every release has a coordinate:

```text theme={null}
tap/publisher/template@version
community/acme/incident-response@1.0.0
```

Tap, publisher, and template names use lowercase slug-safe identifiers. A
released coordinate is immutable: never publish different bytes under the same
coordinate.

## 1. Create a workspace and keys

```bash theme={null}
llmwiki template publish init ./my-tap --tap community --publisher acme
```

This creates separate Ed25519 keypairs for the tap root and the publisher, writes them
`0600` under `my-tap/keys/`, and records a workspace at sequence 0. Private keys are never
printed and are never overwritten. The command prints each public key's SHA-256
fingerprint — publish those fingerprints through the channel your users trust.

<Warning>
  The workspace holds private keys. Never serve it, and never point `--out` inside it:
  `build` refuses that, because it would publish your signing keys.
</Warning>

## 2. Sign a package

```bash theme={null}
llmwiki template publish add ./incident-response.json \
  --workspace ./my-tap --package-version 1.0.0
```

`add` validates the package with the same validator a consumer runs, derives its
coordinate and payload digest, signs the package claim, and records the coordinate as
immutable: the same coordinate may never resolve to different bytes.

## 3. Build a distribution

```bash theme={null}
llmwiki template publish build \
  --workspace ./my-tap --expires-in 30d --out ./dist
```

`build` advances the sequence, signs the index, writes packages before the index, and
**verifies the complete tree as a consumer would before publishing it**. Only then does it
swap the tree into `--out` and commit the new sequence. A failed build leaves the
workspace at its old sequence, so a retry is a clean re-run.

To renew a distribution whose lifetime is expiring without changing its contents, rebuild
with `--refresh`. It republishes the same tree under a fresh `--expires-in` window and
advances the sequence. Without it, a build that finds nothing changed is refused, so an
accidental rebuild never burns a sequence.

Serve `./dist` from ordinary HTTPS hosting.

## 4. Rotate and revoke

```bash theme={null}
llmwiki template publish rotate --workspace ./my-tap --publisher-key-id acme-publisher-2027-01
llmwiki template publish revoke --workspace ./my-tap --package-digest sha256:... --reason "superseded"
```

Rotations and revocations are **staged**, then signed by the next `build`. This is not an
implementation detail: a rotation claim carries the sequence of the index that publishes
it, and that sequence is only known at build time.

<Warning>
  A **tap-root** rotation is carried by exactly one index. A client that does not refresh
  while that index is the published one cannot verify the new root: it must forget and
  re-add the tap, re-pinning your key through your trusted channel. Publisher-key rotation
  has no such window — a client can walk the retained chain from any later index. Keep a
  tap-root rotation published long enough for users to refresh, and announce the new
  fingerprint out of band.
</Warning>

A publisher-key rotation **re-signs every package** with the successor key. It must:
a package signed by a retired key stops verifying the moment the index announces its
successor, because a verifier resolves the publisher key by id from the current index.
The payload never changes, so digests and filenames are stable.

Revoking the *active* publisher key requires rotating to a successor in the same build —
an index that announces a revoked key is one no client will accept.

## 5. Verify before you publish

```bash theme={null}
llmwiki template publish verify ./dist \
  --tap community \
  --key-id community-tap-2026-01 \
  --key-file ./trusted/community-tap-public-key.txt
```

Pass the key from where you *distribute* it, not from inside the tree being verified: a key
carried by the tree it verifies proves only self-consistency.

`publish verify` checks one self-contained snapshot and refuses an index carrying
rotations, because a latest-snapshot-only directory holds no pinned keys to walk a chain
from. That is a scope limit, not a defect: `build` already verified the rotation against
your workspace's own key history, and a real client verifies it against the keys it pinned
from your previous release.

***

# Protocol appendix

The rest of this guide is the wire contract. You do not need it to publish with the CLI
above; it exists so anyone can implement the protocol independently, and so you can audit
exactly what the CLI signs.

## Create separate signing keys by hand

Use separate Ed25519 keys for the tap and each publisher. The tap key signs the
catalog snapshot. A publisher key signs that publisher's package claims.

This Node.js example creates SPKI public bytes and PKCS8 private bytes in the
base64 representation used by the protocol:

```js theme={null}
import { generateKeyPairSync } from "node:crypto";

const { publicKey, privateKey } = generateKeyPairSync("ed25519");

console.log(JSON.stringify({
  publicKey: publicKey.export({ format: "der", type: "spki" }).toString("base64"),
  privateKey: privateKey.export({ format: "der", type: "pkcs8" }).toString("base64")
}, null, 2));
```

Store private keys in a secrets manager or offline signing environment. Do not
leave the example's private-key output in shell history or a shared build log.
Publish only the SPKI public bytes. Assign stable, globally unambiguous key ids such as
`acme-publisher-2026-01` and `community-tap-2026-01`.

The tap root key must reach users through a trusted channel independent of the
tap server. Users provide it explicitly when adding the tap; llmwiki does not
use trust on first use.

## 2. Build the template payload

The signed payload is an ordinary `ProfileTemplatePackage` with
`sourceType: "remote"`. The template id and profile id must match.

```json theme={null}
{
  "schemaVersion": 1,
  "templateId": "incident-response",
  "version": "1.0.0",
  "displayName": "Incident Response",
  "publisher": "acme",
  "sourceType": "remote",
  "license": "MIT",
  "minLlmwikiVersion": "1.0.0",
  "description": "Track incidents, evidence, mitigations, and reviews.",
  "profile": {
    "schemaVersion": 1,
    "profileId": "incident-response",
    "displayName": "Incident Response",
    "entities": {
      "incidents": {
        "directory": "wiki/incidents",
        "titleField": "title",
        "requiredFields": ["title"],
        "fields": {
          "title": { "type": "string", "required": true }
        }
      }
    }
  }
}
```

During authoring, test a copy with `sourceType: "local"` through `llmwiki
template init --file`. Restore `sourceType: "remote"` before signing the release
payload. Remote installation repeats package validation, profile validation,
connector-binding checks, and minimum-version checks, so a signature cannot make
an invalid package installable.

## 3. Canonicalize, digest, and sign the package

All digests and signatures use RFC 8785 canonical JSON bytes. Do not sign
pretty-printed JSON or rely on object insertion order. The implementation uses
the `canonicalize` npm package.

```bash theme={null}
npm install canonicalize
```

The publisher signs the canonical claim `{ coordinate, payloadDigest }`, not
the full envelope:

```js theme={null}
import { createHash, createPrivateKey, sign } from "node:crypto";
import canonicalize from "canonicalize";

function canonicalBytes(value) {
  const text = canonicalize(value);
  if (text === undefined) throw new Error("value cannot be canonicalized");
  return Buffer.from(text, "utf8");
}

function digest(value) {
  return `sha256:${createHash("sha256").update(canonicalBytes(value)).digest("hex")}`;
}

function signature(claim, keyId, privateKeyBase64) {
  const key = createPrivateKey({
    key: Buffer.from(privateKeyBase64, "base64"),
    format: "der",
    type: "pkcs8"
  });
  return {
    keyId,
    algorithm: "ed25519",
    value: sign(null, canonicalBytes(claim), key).toString("base64")
  };
}

const coordinate = "community/acme/incident-response@1.0.0";
const payloadDigest = digest(payload);
const envelope = {
  schemaVersion: 1,
  coordinate,
  payload,
  payloadDigest,
  publisherSignature: signature(
    { coordinate, payloadDigest },
    "acme-publisher-2026-01",
    publisherPrivateKeyBase64
  )
};
```

Write the envelope as JSON to:

```text theme={null}
packages/sha256/<payloadDigest-without-sha256-prefix>.json
```

The filename and signed index both identify the canonical payload digest. The
client recomputes it and refuses any mismatch.

## 4. Build and sign the tap index

The index lists publisher public keys and every release currently available
through the tap:

```json theme={null}
{
  "schemaVersion": 1,
  "tap": "community",
  "sequence": 1,
  "generatedAt": "2026-07-13T12:00:00Z",
  "expiresAt": "2026-08-12T12:00:00Z",
  "publishers": {
    "acme": {
      "keyId": "acme-publisher-2026-01",
      "publicKey": "<base64-spki-public-key>"
    }
  },
  "packages": [
    {
      "coordinate": "community/acme/incident-response@1.0.0",
      "publisher": "acme",
      "payloadDigest": "sha256:<64-lowercase-hex-characters>"
    }
  ],
  "rotations": [],
  "revocations": [],
  "signature": {
    "keyId": "community-tap-2026-01",
    "algorithm": "ed25519",
    "value": "<base64-signature>"
  }
}
```

Construct the object without `signature`, sign that complete unsigned object
with the tap private key using the same `canonicalBytes` and `signature`
helpers, and then add the resulting signature field.

For every new snapshot:

1. Increase `sequence`; never reuse or decrease it.
2. Set valid UTC `generatedAt` and `expiresAt` values.
3. Keep every coordinate bound to its original digest forever, even if it is
   temporarily absent from a later index.
4. Publish package envelopes before replacing `index.json`.
5. Replace `index.json` atomically at the hosting layer.

The index is limited to 10,000 entries per bounded collection and 4 MiB. A
package envelope is limited to 2 MiB.

## 5. Verify the distribution offline before publishing

Before uploading anything, verify the built tree exactly as a client would verify
the bytes it downloads. This runs offline, reads only, and writes nothing.

```bash theme={null}
llmwiki template publish verify ./dist \
  --tap community \
  --key-id community-tap-2026-01 \
  --key-file ./community-tap-public-key.txt
```

The directory must be the exact static tree you intend to serve, and nothing else:

```text theme={null}
dist/
  index.json
  packages/
    sha256/
      <payload-digest-hex>.json
```

Any extra, missing, or symlinked entry is a failure, not a warning. Each package
filename must be the hex payload digest of the entry that the signed index names.

The command re-runs the same parsers, Ed25519 verification, continuity, revocation,
and profile validation that a real client runs, then prints bounded provenance:

```text theme={null}
Verified template publisher distribution.
Scope: snapshot
Continuity: not_applicable_no_rotations
Tap: community
Sequence: 7
Tap key: community-tap-2026-01
Packages: 3
```

Use `--json` for the same result as a stable object. Failures exit non-zero with a
bounded reason and never echo file contents or local paths.

<Warning>
  Pass the tap public key from wherever you *distribute* it, not from inside the
  distribution you are checking. A key carried by the same tree it verifies proves
  only that the tree is self-consistent. Verification is meaningful only when the
  key reaches the verifier through the independent channel your users trust.
</Warning>

**Snapshot scope.** This command verifies one self-contained snapshot. It refuses any
index carrying `rotations` or a `tapKeyRotation`, because a rotation chain is only
meaningful against a client's previously pinned key, and a fresh offline check has no
pins to walk from. That is why the output says `not_applicable_no_rotations`. Verify a
rotation-bearing index the way a user experiences it: from a clean client that already
pinned the old key, as in the next section. See [Rotate or revoke deliberately](#7-rotate-or-revoke-deliberately).

## 6. Verify from a clean client

Distribute the tap public key, its fingerprint, and the index URL through an
independent trusted channel. Then test exactly what users will run:

```bash theme={null}
llmwiki template tap add community \
  https://templates.example/community/index.json \
  --key-id community-tap-2026-01 \
  --key-file ./community-tap-public-key.txt

llmwiki template tap refresh community
llmwiki template search incident-response --tap community
llmwiki template verify community/acme/incident-response@1.0.0
```

Test installation in an empty project:

```bash theme={null}
mkdir verify-install && cd verify-install
llmwiki template init community/acme/incident-response@1.0.0
llmwiki template status
llmwiki profile validate
```

The verification client checks the tap signature, sequence, expiry, publisher
key continuity, revocations, package digest, publisher signature, coordinate
identity, template manifest, and profile contract before installation.

## 7. Publish an update

Never replace an existing release. Create a new payload with a higher semantic
version, sign a new envelope, upload it at its new digest path, and publish a
higher-sequence index containing the new coordinate.

Users can then preview and apply the update:

```bash theme={null}
llmwiki template tap refresh community
llmwiki template status
llmwiki template update --to 1.1.0 --dry-run
llmwiki template update --to 1.1.0 --yes
```

The update planner refuses local profile drift, incompatible existing content,
pending review candidates, active workflow runs, or changed tap authority. The
apply path journals the old profile and provenance so interruption is
recoverable.

## 8. Rotate or revoke deliberately

Publisher key rotation is a signed chain. Each rotation record identifies the
publisher, old key id, new public key, and effective sequence. The canonical
rotation claim is signed by both the old and new private keys. Retain the
rotation history in later indexes so clients that skipped snapshots can walk
from their pinned key to the current key.

Tap-root rotation uses the same dual-signature principle through
`tapKeyRotation`. Users who cannot verify a cooperative rotation must explicitly
forget and re-add the tap, which resets its trust history.

<Note>
  An index carrying `rotations` or a `tapKeyRotation` is refused by
  `llmwiki template publish verify`, which checks one self-contained snapshot and
  holds no pinned keys to walk a chain from. This is a scope limit, not a defect in
  your index. Verify rotation-bearing snapshots from a clean client that already
  pinned the previous key, exactly as your users will experience the rotation.
</Note>

To revoke evidence, add a `revocations` record with kind `package` and the
payload digest, or kind `publisher-key` and the key id. Include a reason and UTC
`revokedAt` timestamp. Revocations accumulate monotonically in client state;
removing one from a later index does not restore trust.

## Release checklist

* The payload is declarative and validates with the target llmwiki version.
* `templateId`, `profileId`, publisher, and coordinate agree.
* The package claim and index use RFC 8785 canonical bytes.
* Package files are uploaded before the new index becomes visible.
* The sequence increased and the expiry window is intentional.
* The tap root key is distributed outside the tap server.
* A clean client can refresh, verify, install, and report clean status.
* Previous coordinates still resolve to their original immutable bytes.

The repository's [offline protocol fixture](https://github.com/atomicstrata/llm-wiki-compiler/tree/main/test/fixtures/template-registry)
is the compatibility oracle for envelope and index implementations.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.