.facet file is a two-layer container. Both layers are tar archives, and only the inner layer is compressed.
Outer layer
An uncompressed tar with exactly two entries.JSON
required
facetVersion (the archive format this artifact conforms to), archive (the inner entry name), integrity (the canonical fingerprint as sha256: and 64 hex characters), and files (a map of every inner-archive entry path to its sha256:<hex> hash).The current format is 0.2, where archive MUST be the literal archive.tar.gz and per-entry hashes live in files. The legacy 0.1 format is accepted during the compatibility window and carries an assets map instead. The two are dispatched by exact match on facetVersion, never by ordering, and neither shape is reinterpreted as the other.gzip
required
The gzip-compressed inner archive carrying the content.
Inner archive
The inner archive decompresses to a deterministic uncompressed tar containing the embeddedfacet.json, every declared text asset at its conventional path, and every declared supplementary file at its declared path. Entries MUST be sorted lexicographically by path.
The entry set is derived from the embedded manifest. There is no recursive discovery, so archive membership stays explicit.
Determinism
Tar metadata MUST be fixed so build output is byte-identical across builds, machines, and platforms:mtime 0, uid 0, gid 0, mode 644, and empty user and group names. The integrity hash covers these bytes, so any producer that wants its artifacts to interoperate MUST use the same metadata.
The canonical fingerprint is the SHA-256 of the uncompressed inner tar. Gzip output is never hashed, because different gzip implementations produce different bytes that decompress to identical tars. See the Integrity Model.
A verifier MUST recompute the fingerprint by decompressing and hashing. The build manifest’s own integrity is a claim to check, never a value to trust.
Content rules
A verifier MUST reject an archive that violates any rule below.1. Raw header validation
Both tar layers MUST be validated at the raw-header level, before any path-keyed map is built, so a malformed entry cannot alias a legitimate one. A verifier MUST reject:- duplicate paths, and paths that collide after the portable fold;
- non-regular entries: symlinks, hard links, directories, and devices;
- a ustar header checksum mismatch;
- non-zero bytes after the end-of-archive marker;
- a name or prefix that does not round-trip as UTF-8;
- a non-empty ustar
prefixfield; - GNU base-256 size encoding.
facet.json and build-manifest.json MUST be rejected before schema validation.
2. Path safety
Every key infiles and every inner entry name MUST be a safe, portable relative path. The grammar forbids traversal (..), empty and . segments, absolute paths, drive and URL prefixes, backslashes, NUL and control bytes, the reserved characters < > : " | ? *, Windows device names such as CON and COM1, and a trailing dot or space on any segment.
Paths that collide after the portable fold, and any file against directory prefix conflict, are rejected too.
3. Membership
For the current format, the plan derived from the embeddedfacet.json, the inner tar entry set, and the files keys MUST all be the same set, and each entry’s recomputed hash MUST equal the hash files records.
The expected set is derived from the embedded manifest, never from the build manifest, so the embedded manifest is the trust root. A files record cannot expand archive membership: a key naming a path the manifest does not declare is rejected outright.
Legacy 0.1 archives are checked by reconciling declared against present entries in both directions, then confirming outer exclusivity against the embedded manifest.
This is what keeps the supply-chain guarantee intact: an archive cannot carry an undeclared file that lands on disk at install time.