Repository navigation
Philter 4 is a major release, and upgrading from 3.x takes planning. It is now API-only and runs on Java 25, Spring Boot 4, and Phileas 4.5. The built-in UI has been removed; Philter UI is a separate project still in development. A single instance now stores its data in SQLite by default with no database server, and MongoDB remains an option when several instances need to share data. Philter 4 also adds an evidence trail: a tamper-evident redaction ledger with legal holds, each redaction stamped with its policy version, optional output signing, and an exportable audit log. Security defaults are stricter throughout: HTTPS by default, scoped API keys, encryption of stored policies, and a required PHILTER_ENCRYPTION_KEY. Many API responses change shape, so existing integrations need updating. See the upgrade guide for each change.
Added
- Evidence trail: a tamper-evident redaction ledger with legal holds, policy versions stamped on
each redaction, opt-in output signing, and an exportable audit log. - Administration over the API, with scoped API keys and a bootstrap key
(PHILTER_BOOTSTRAP_API_KEY) for the first administrator. - Sign-in for a separate user interface: passwords, expiring session keys, TOTP MFA, and lockout,
off unlessPASSWORD_SIGN_IN_ENABLED=true. See Sign-in Security. - SQLite storage, the default: a single Philter instance keeps all its data in one file
(SQLITE_PATH) with no database server. SetSTORAGE_TYPE=mongodbfor MongoDB, which several
instances can share. With SQLite, a lock left held by a Philter that stopped part way through an
operation is cleared at the next start and recorded asstorage_guard_recovered. See
Database and the
SQLite quick start. - Moving from SQLite to MongoDB:
java -jar philter.jar migrate-storage --to mongodb, run with
Philter stopped, copies a SQLite database to an empty MongoDB as stored (ids, ledger signatures,
and encrypted fields unchanged), verifies the copy, and records it asstorage_migrated. See
Moving from SQLite to MongoDB. - PII counts can be sorted:
GET /api/pii-countstakessort(day,context, ordocuments) andorder, as the
other listings do. See PII Counts API. - Limits API:
GET /api/limitsreturns the limits and rules Philter enforces and what the caller
may do, andGET /api/sign-ingives a sign-in page the password rules. - Per-list redact list endpoints:
GETandPUT /api/redact-lists/{list}read and replace the
always-redact or never-redact list on its own, withIf-Matchon the list's revision so concurrent
edits are refused rather than lost. - Policy templates:
GET /api/policies/templates/{templateName}returns a starting point for a new
policy;defaultis the template new users' default policy is created from.templatesis now a
reserved policy name. - Webhook test and delivery history:
POST /api/webhook/testsends a signed test event and reports
the outcome,GET /api/webhook/deliverieslists deliveries with their status and last error, and
PUT /api/webhookkeeps the existing secret when none is sent. - Deleted policies and version authors:
GET /api/policies?deleted=truelists deleted policies
whose history is kept, with when and by whom each was deleted, and each policy version names the
user who made it. - Audit actors and user ids: each event from
GET /api/auditnames its actor'susername, and
user responses include the user'sid. - Change a user's email:
PUT /api/users/{username}/emaillets an administrator set, change, or
remove a user's email address. Email addresses are now checked for a basic format, including when a
user is created. - Schema versions and automatic migrations: Philter records the version of its data layout and
migrates the data at startup when a release needs it, one instance at a time; it refuses to start on
data written by a newer release. Back up the database before upgrading. See
Upgrading. - Storage selection:
STORAGE_TYPEselects where Philter stores its data:sqlite, the default,
ormongodb. Philter reaches its data only through storage interfaces, so other storage can be
added later. Philter refuses to start whenMONGODB_CONNECTION_STRINGis set without
STORAGE_TYPE, rather than guess which storage you meant. - Policy diffs show both sides: each
replaceandremovefromGET /api/policies/{name}/diff
carriesoldValue, the value before the change. - Filters for ITINs, EINs, and Canadian SINs:
itin,ein, andcanadaSinreport US Individual
Taxpayer Identification Numbers, Employer Identification Numbers, and Canadian Social Insurance
Numbers as their own types. Thessnfilter reports neither ITINs nor values in the EIN form
NN-NNNNNNN, so a policy, including the managed policies, must enableeinto detect EINs. See
Filters. - Asynchronous PDF redaction with signed webhooks.
- Client certificates (mutual TLS): when Philter serves HTTPS itself,
SSL_CLIENT_AUTH=needwith
SSL_TRUSTSTORErequires every client to present a certificate issued by one in the trust store,
in addition to its API key. See Client certificates. - Fairness keys on queued documents: an asynchronous PDF request can pass
fairness_key, and
documents one account queues with different keys take turns, as different accounts do. See
Fairness keys. - The policy behind a queued document: document status and listing responses include
policyName,
policyVersion, andpolicyContentHash, and that revision can be read through the policy versions API. See
Which policy redacted a document. - Local PhEye models: a ph-eye filter with
modelPathruns a GLiNER model in-process through
ONNX Runtime, with no PhEye service.modelPathnames a model inPHEYE_MODELS_DIRECTORY, which
GET /api/modelslists, or is a path inPHEYE_ALLOWED_MODEL_PATHS, and
PHEYE_ONNX_THREADSsets its thread count; Philter does not start if that is not a positive whole number.
PHEYE_ONNX_DEVICEcan run it on a GPU, with ONNX Runtime's GPU package in place of the CPU package Philter
includes. - Search queued documents:
GET /api/documentstakesq, matching document IDs and file names by part,
ignoring case, as the ledger listing does, so a document can be found by the file name it was submitted with. See
List Documents. - A processing document is not deleted:
DELETE /api/documents/{documentId}returns409 Conflictwith
Retry-After: 5while the document isPROCESSING, so a delete cannot remove a job out from under the worker
redacting it. APENDINGdocument, which no worker has started, can be deleted. See
Delete. - Phield and Diffuse integrations, off by default.
- Operations: Prometheus metrics, HTTPS by default, an optional shared Valkey/Redis cache, and
trusted-proxy handling. - Re-identifying a text:
POST /api/reidentify/textrestores theCRYPTO_REPLACEvalues in a text,
such as an LLM's response to a redacted prompt, under thereidentifyscope. Tokens that do not
decrypt are left as they are and counted. See
Re-identification.
Changed
- Detection and redaction changes from Phileas 4.5.0, including how dates, cities, and driver's license
numbers are detected and how the date strategies transform dates. See the
Phileas release notes. config.pdf.scaleis deprecated and ignored. A redacted PDF page now keeps the input page's size, where the
default scale of0.25returned it at a quarter of its width and height. A policy that sets it still loads.- A PhEye service that cannot be reached is reported as
503with a message saying how to fix the policy
or the deployment, and an asynchronous document fails with the same message. Previously the request failed
with500andAn unknown error has occurred. - The managed policies
managed_common_piiandmanaged_healthcare_phidetect names with
ph-eye-pii-en-small, as the default policy does. - The default policy detects names with a model inside Philter. Its person-name filter names
ph-eye-pii-en-small(labelname, threshold 0.9), which the Docker image includes in
PHEYE_MODELS_DIRECTORYat a pinned revision, so names are redacted with no PhEye service. This applies
to thedefaulttemplate and to users created from now on; an existing user'sdefaultpolicy is
unchanged. The model is CC-BY-4.0 (trained on NVIDIA Nemotron-PII), and its model card, with the attribution
that license requires, ships beside it. The image is about 620 MB larger. Outside the image, a model by that name must be in
PHEYE_MODELS_DIRECTORY, or redaction with the default policy is refused. - PhEye filters examine all of a long text. A PhEye model reads at most its
max_lenwords (512 for the
ph-eye-pii-en-*models) in one call, and a name after that point in a request was not detected, whether the
model ran in-process or in the PhEye service. The filter now detects in overlapping windows that fit the model.
See Long Text. - The OpenAPI specification matches the responses. The
/api/explainexplanation and its spans, and the signing
key'sjwk, have schemas; the audit export is described as CSV text; and responses that have no body, or a
{message}body, are described as such. The policy diff'soldValue, the hold listing'sowner, and the headers
Philter sends (X-Request-Idon every response,X-Philter-Signature,X-Document-Idon/api/explain,ETag,
the audit export headers, andRetry-Afteron throttled sign-ins and password checks and on refused administrator
changes) are declared. The policy listing's and sign-in's alternative responses are told apart by their required
fields, and the hold and ledger listings each describe one item schema with an optionalowner. - Java 25 is required.
- No built-in UI. Philter UI, in development, is separate.
- PDF redaction is asynchronous by default. Append
?async=falsefor the previous behavior. - Policies Phileas cannot load are refused with 400. A policy is checked by building its filters through
Phileas when it is created, replaced, copied, or rolled back to, and before a redaction or asynchronous
submission uses it. A strategy condition that cannot be parsed or that no filter evaluates, a regular
expression that does not compile or fails its check, or an invalid value such as an unknownsensitivity
returns400 Bad Requestwith Phileas's reason, which never repeats a condition's text. Previously such a
policy was saved and every redaction with it failed with500. See
Filter Strategy Conditions. - A context that does not exist is refused with
400(Context does not exist.) by synchronous/api/filter
and/api/explain, as asynchronous submission already refused it. Previously they returned500. - A policy cannot direct Philter's requests or credentials. A policy that names a ph-eye
endpointother than
PHEYE_ENDPOINTor one inPHEYE_ALLOWED_ENDPOINTS, sets a ph-eyebearerToken, names amodelPaththat is not
a model inPHEYE_MODELS_DIRECTORYor inPHEYE_ALLOWED_MODEL_PATHS, or names a generatorendpointnot inGENERATOR_ALLOWED_ENDPOINTSis refused
with400. Previously any user's policy could send the text being redacted, and the deployment's
PHEYE_BEARER_TOKEN, to a server of its choosing. Requests to these services no longer follow redirects, and
connecting is bounded at 10 seconds. See Redaction services. - A policy can name only files an administrator allows. A custom dictionary's
files, a filter's
ignoredFiles, the top-levelignoredfiles, and aMAP_REPLACEstrategy'smappingFilesmust be regular
files insidePOLICY_FILES_DIRECTORIESand no larger thanPOLICY_FILES_MAX_BYTES(default 10 MB). Unset, a
policy may name no file. A policy naming any other file is refused with400. Previously any user's policy could
read any file the server could read, and a device such as/dev/zerocould exhaust its memory. See
Policy files. - A policy can name only environment variables an administrator allows. A
cryptoorfpekey or tweak given
asenv:NAMEmust name a variable inPOLICY_ALLOWED_ENV_KEYS; unset, a policy may name no variable, and Philter's
own secrets can never be listed. A policy naming any other variable is refused with400, including by
/api/reidentify. Previously any user's policy could use any variable in Philter's environment as its key,
including a key meant for other policies or one of Philter's own secrets. See
Environment variables in a policy. token !=conditions apply. A strategy with a condition such astoken != "127.0.0.1"was
never applied before; it now applies to every value other than the one named, ignoring case. Review
policies that usetoken !=. See Filter Strategy Conditions.- Outbound HTTPS verifies certificates.
TLS_TRUST_ALL_ENABLED=truerestores the previous behavior. POST /api/policiesonly creates. A name in use returns409; replace a policy with
PUT /api/policies/{name}.- Errors are JSON with a
messageand areasoncode, and afieldwhen an invalid parameter is
known, from every refusal, including authentication, scope checks, and the audit export. A404
that would otherwise reveal whether an account exists says onlynot_found, the same as any other. - Users are deactivated rather than deleted, keeping their data and ledger evidence.
- Listings return an object with
total, and acceptsort,order, and, where it applies, aq
search.GET /api/policies,/versions,/api/holds, and/api/listsreturned bare arrays;
GET /api/listsis now paged.GET /api/usersfilters byroleandactive,GET /api/documents
bystatus, and the ledger'sall_userslisting acceptsq. - Custom lists are written as a JSON object.
POSTandPUT /api/lists/{name}take
{"items": [...], "description": "..."}rather than a bare array withdescriptionin the query string, which is
now refused with400. A description can have up to 200 characters, reported as
customLists.descriptionMaxLengthbyGET /api/limits.GET /api/lists/{name}returns the items initems, as
they are sent, rather than inlists. - Creates return
201and deletes204.POST /api/contextsreturns201 Created. Deleting a context, its
entries, one entry, a document, a ledger chain, a policy, or releasing a legal hold returns204 No Contentwith no
body, as deleting a custom list, API key, or webhook already did. The ledger purge and session key revocations still
return200with what they removed. - A document's ledger chain is read a page at a time.
GET /api/ledger/{documentId}takesoffsetandlimit
(25 by default, at most 100) and returnstotal, how many entries the chain has;validstill covers the whole
chain. Export the chain to read every entry at once. - Listing totals are 64-bit.
totalfromGET /api/ledgerandGET /api/contexts/{name}/entriesis anint64
in the OpenAPI specification, as every other listing's is. - A context is created with a JSON body.
POST /api/contextstakes{"name", "entityTypeDisambiguation", "disambiguationScope", "ledger"}, the namesGETandPATCH /api/contexts/{name}use, rather than query
parameters; a name or setting sent as a query parameter is refused with400. - A context's settings are changed with
PATCH /api/contexts/{name}and a JSON body.PUTwith the settings in
the query string is removed. The body takesentityTypeDisambiguation,disambiguationScope, andledger, the names
GET /api/contexts/{name}returns, and a setting sent as a query parameter is refused with400. Thecontext_updated
audit event names the changed settings the same way, such asentityTypeDisambiguation: true. - Documents are listed in
documents, each with atype.GET /api/documentsreturns its items indocuments
rather thanpendingRedactedDocuments, since the listing includes finished and failed documents too. Each item, and
the response fromGET /api/documents/{documentId}/status, has atype;redactionis the only type. So does each
document webhook's payload. A webhook receiver should ignore events it does not recognize, since later releases
may add some. - Policies and contexts are listed as objects. Each item from
GET /api/policieshas the policy'sname,
description,revision,createdAt, andupdatedAt, and each fromGET /api/contextsthe context'snameand
createdAt, rather than a bare name; withall_users, each also has itsowner. Read the name fromname. - Lists, contexts, and holds are deleted by path only.
DELETE /api/lists?name=,
DELETE /api/contexts?name=, andDELETE /api/holds?reference=are removed: names are checked when
they are created, soDELETE /api/lists/{name},/api/contexts/{name}, and/api/holds/{reference}
always reach them. - Request ids: every
/apiresponse returnsX-Request-Id, and the audit events the request causes
record the same id, so an error a client sees can be found in the audit log. Redaction events keep
using the document id, so a queued redaction's start and completion share one id. - Manage your own sessions: session key listings show each session's client address and user agent
(kept only while the session lasts) and mark the current one, andDELETE /api/users/me/session-keys
signs out every other session. - Stable audit log export paging:
GET /api/audit/exportreturnsX-Philter-Export-Next-Cursor
and acceptscursor, which continues after the last event of the page before. Paging by cursor
returns each event once even while events are being written, including each page's own
audit_log_exportedevent.offsetstill works but can repeat events on a range that includes the
current day. - No lost updates on policy edits: policy reads return an
ETagcovering the policy and its
description and notes. Send it inIf-MatchonPUT /api/policies/{name},
PUT /api/policies/{name}/details, orPOST /api/policies/{name}/rollback, and the write is refused
with409and reasonpolicy_changedif the policy changed since. Requests withoutIf-Matchwork
as before. Withdetails=true,POSTandPUT /api/policiestake the description and notes in the
same body as the policy, written together.
Removed
GET /api/status(useGET /api/health, which reports"status": "UP") and OpenSearch.
Security
- The compose files no longer publish PhEye or MongoDB. PhEye has no authentication, and the bundled MongoDB's
root password is in the file; Philter reaches both on the compose network. Only Philter's port 8080 is published. - The compose files require
PHILTER_ENCRYPTION_KEY. They fell back to a key published in the repository,
which unwraps every encrypted record../compose.shgenerates one in.env; without it,docker composestops
with a message saying so. - The compose files require
PHILTER_BOOTSTRAP_API_KEY. They fell back to an administrator key with every scope
published in the repository../compose.shgenerates one in.env. - The generated TLS keystore is readable only by Philter's user. The Docker image wrote it world-readable, and its
default password is public, so anyone who could read the volume had the private key. PHILTER_ENCRYPTION_KEYis required. Back it up: data cannot be recovered without it.- Policies are encrypted at rest. A policy's content, every revision of it, and its description and notes are
encrypted underPHILTER_ENCRYPTION_KEY, as list items and ledger tokens are, since a policy can hold the terms it
redacts, condition values, and keys. Names and content hashes stay readable. See
What is encrypted at rest. - Only loopback is a trusted proxy by default.
TRUSTED_PROXIESdefaulted to every private and link-local range,
so any client on an internal network could setX-Forwarded-Forand choose the address recorded in the audit log
and counted by the sign-in rate limit. The default is now127.0.0.0/8, ::1. Behind a load balancer on another
host, setTRUSTED_PROXIESto its addresses; Philter logs once when it ignores the header. See
API Access. - PII counts are kept per user. Counts were bucketed by context name alone, so two users' contexts of one name
were counted together, and the PII Counts API showed every user's context names to any administrator. Counts are
now recorded per user's context and day; each entry has a stableid, and another user's context name and username
are returned only withADMIN_CROSS_USER_ACCESS_ENABLED, while their counts still are. Anownerfilter was
added. See PII Counts API. - Failure messages no longer reveal internal details. A failed document's
errorand its
DOCUMENT_REDACTION_FAILEDwebhook carried the raw exception message, which could name the model service's host and
port or a policy snapshot hash; webhook test results and delivery errors carried the HTTP client's message, with the
addresses a host resolved to. A failure the caller can act on keeps its message, and any other is now reported
generically or by its kind, with the full error in Philter's log. See
Documents API and
Delivery errors. - Context names are no longer written to the application log. Creating a context's stored service, deleting a
context, and evicting an entry to honorMAX_CONTEXT_SIZElogged the context's name, which is the user's own label
and can describe whose data it holds. These messages now name only the owner's user ID, and a request refused for a
missing scope is logged by its route (such as/api/contexts/{name}) rather than its path. The audit log still
records context names. See Redaction Contexts. - Built filters are cached by a SHA-256 hash of the policy. Every user's redactions share Phileas's filter
cache, which was keyed by a 64-bit FNV-1a hash of the policy. That hash is not designed to resist deliberately made
collisions, so a policy could in principle be written to take another user's key and be filtered with that user's
dictionaries and keys. Phileas 4.5 keys the cache with SHA-256. - What Philter stores in Valkey is encrypted and authenticated. Cached API key records were readable JSON under
names derived from the key, and Philter trusted them without checking the database, so anything that could write to
the cache could copy a record to the hash of a key of its choosing and use that key as the record's user, an
administrator included. Values are now encrypted with AES-256-GCM under a key derived from
PHILTER_ENCRYPTION_KEYand bound to their names, and names are keyed hashes; a value that does not open, or was
written back after it expired, reads as absent and Philter uses the database. Philter also warns at startup when a
cache on another host is used withoutCACHE_PASSWORDorCACHE_SSL. Existing cache entries are ignored after
upgrading. See Protecting the cache. - A wrong current password counts toward sign-in lockout.
PUT /api/users/me/passwordchecked the current
password without any limit, so a key holdingusers:write, such as a stolen session key, could be used to guess its
user's password. A wrong current password now counts as a failed sign-in, a locked user cannot change their password
either (429,reason: locked), and the lock's audit event names the key that made the guesses. See
Users API. The API documentation now also
says which limits Philter enforces and recommends per-key limits at a proxy for re-identification and ledger
export. See Rate limiting. - What an asynchronous redaction keeps is documented and tested. The Documents API documentation now says what
Philter keeps for a queued document and for how long, and storage tests on MongoDB and SQLite confirm that the
submitted and redacted documents are encrypted at rest, that deleting a document removes its record, and that a
failed job, including one abandoned after its final recovery, keeps no copy of the submitted document. See
Documents API. - Filling the cache can no longer lock everyone out of sign-in. Sign-in counts shared the in-memory cache with
cached data, and when it was full every username was treated as locked, so one user redacting a document with many
distinct values could lock all sign-ins. An account's failures and lock are now stored with the user, where no other
traffic can push them out; usernames with no account and the per-address rate limit use a store of
their own; IPv6 clients are rate limited per /64; and a full in-memory store drops its entries nearest to expiring
instead of refusing. See Lockout and rate limiting. - Tomcat no longer reads
X-Forwarded-Foron Kubernetes. On a detected cloud platform, Spring Boot trusted the
header from any private address beforeTRUSTED_PROXIESwas consulted. OnlyTRUSTED_PROXIESnow decides. - Only the user who set a legal hold, or an administrator, can release it. A user could release a hold an
administrator placed on their evidence; that is now refused with403(admin_required). See
Legal Holds API. - PDFs are bounded before rendering. A small PDF with a huge page, or a policy with a high
dpi, could exhaust
memory. A PDF overPDF_MAX_PAGES(default 2000) or with a page overPDF_MAX_PIXELS_PER_PAGE(default 50 million
pixels) is refused with413, as is one using an image over that size or a compressed stream that decompresses past
PDF_MAX_STREAM_BYTES(default 256 MB). At mostPDF_MAX_CONCURRENTPDFs (default 2) are redacted at once per
instance; a synchronous PDF over that is refused with503. The Docker image gives the JVM 75% of the container's
memory. See Redaction Engine and
Memory. - Caller-supplied values cannot forge audit fields. Names, references, document IDs, usernames, and the
re-identification reason were written into an event'sdetailsas is, so a value such asx, owner: adminread as
extra fields. A value that could be misread is now written as a quoted, escaped string. See
Auditing. - Administrators can clear a sign-in lock.
POST /api/users/{username}/sign-in/unlockclears a user's lock
and failure count beforeSIGN_IN_LOCKOUT_MINUTESends, audited assign_in_unlocked. Lockout is per username, so
anyone who can reach sign-in can lock out a known username; Network placement
describes restricting who can. - Starting MFA enrollment takes the current password. A stolen session key could enroll an authenticator of its
own, and the person could then not sign in until an administrator removed it.POST /api/users/me/mfanow takes
password, and a wrong one counts toward the sign-in lockout. A user without a password cannot enroll. See
Start enrollment. - Every request body is size-limited. Only
POSTandPUTwere, so aPATCHbody had no limit.
MAX_FILE_SIZE_BYTES_OTHERnow applies to every request except a document sent to/api/filteror/api/explain.
An oversized JSON body sent to an endpoint that reads it into an object, such asPOST /api/reidentify, is now a
413rather than a400. - Webhooks refuse every special-purpose address. Without an allowlist, a webhook could reach shared address
space (100.64/10, which holds Alibaba Cloud's metadata service at 100.100.100.200), reserved and benchmarking
ranges, and private IPv4 addresses through NAT64, 6to4, or IPv4-compatible IPv6 forms. These are now refused
unless an administrator lists them; NAT64 and 6to4 are judged by the IPv4 address they lead to, so public endpoints
still work. See Where a webhook may point. - Policy and redact list changes apply to the next redaction. A change to the always/never redact lists used
to take up toREDACTION_CACHE_TTL_SECONDS(60 seconds) to apply, so a term added to always-redact, or removed
from never-redact, kept being let through; a policy edit did the same on instances other than the one that made
it. Redaction now uses its cached copy only while the stored revisions match, on every instance. See
Caching. - Context token hashes are keyed per user. Each user's context tokens are hashed under a key derived from
PHILTER_ENCRYPTION_KEYand their username, so one user cannot match another's exported hashes by redacting
guesses in a context of their own, or import another's export to learn what its replacements stand for. An export
now matches only in contexts of the same username under the same key: within a deployment, or carried to another
with the same key and username, such as from staging to production. See
Exporting and Importing Mappings. - A queued document's captured configuration is kept only with the job. It holds the resolved policy,
list contents, and keys, is encrypted, and is removed when the job expires or is deleted; no other copy is kept. - Cross-user access is opt-in through
ADMIN_CROSS_USER_ACCESS_ENABLED. While it is off, an administrator can
create an API key for another user only as that user's first key, so the setting cannot be bypassed by creating
a key for a user who already holds data and acting as them. With it on, such a key and every read of another
user's policies, custom lists, documents, and ledger throughownerare recorded asadmin_cross_user_access;
before, only changes and some reads were. - Ledger exports contain original values and must be treated as sensitive.
- Access is decided by the path a request is routed by. Authentication, scope checks, request size limits,
and content verification all use the decoded path, so a percent-encoded path such as/%61pi/documentsis
treated exactly as/api/documents. Before this, such a path reached the endpoint without the scope check or
the live checks on the key, so a key could use endpoints outside its scopes and a deactivated user's key or a
restricted session key kept working.
Downloads
- Docker image:
philterd/philter:4.0.0 - Jar: https://downloads.philterd.ai/artifacts/philter-4.0.0.jar (signature:
philter-4.0.0.jar.asc)
Full Changelog: 3.4.2...4.0.0