Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ upstream history, authorship, licenses, and notices.

## Current release

Server `v1.6.520` packages Web Console `1.6.181`
and Orchestration Engine `v0.183.336`. API Keys support full, custom and closed
Server `v1.6.521` packages Web Console `1.6.181`
and Orchestration Engine `v0.183.337`. API Keys support full, custom and closed
policies, optional expiry, and key-scoped durable audit records. Their permissions
remain bounded by the owner's current role and environment access. Delegated
terminal/log access requires the verified Host API audit capability and the
Expand All @@ -35,7 +35,10 @@ Audit export closes its own menu before locking the request, allowing consecutiv
JSON, CSV and XLSX downloads without leaving a disabled menu open.
Key scope choices use the viewer's authorized environment list. Environment,
stack and resource selectors close before dependent loading or verification.
See the [release note](docs/releases/server-1.6.520.md) for exact
Failed Key edits use a consistent public Key ID in audit records. Per-Key audit
also reads historical numeric-ID records within the same ownership and environment
authorization checks; existing records are not rewritten.
See the [release note](docs/releases/server-1.6.521.md) for exact
component identities and verification boundaries. A version or packaged feature
does not establish that every resource/role/hardware combination has been tested.

Expand Down Expand Up @@ -70,15 +73,15 @@ docker run -d --name pasturestack-server --restart unless-stopped -p 8080:8080 \
-v pasturestack-cattle:/var/lib/cattle \
-v pasturestack-mysql:/var/lib/mysql \
-v pasturestack-mysqllog:/var/log/mysql \
ghcr.io/pasturestack/server:v1.6.520
ghcr.io/pasturestack/server:v1.6.521
```

For HTTPS termination at a reverse proxy, set the exact public origin:

```yaml
services:
pasturestack-server:
image: ghcr.io/pasturestack/server:v1.6.520
image: ghcr.io/pasturestack/server:v1.6.521
restart: unless-stopped
ports:
- "8080:8080"
Expand Down
32 changes: 32 additions & 0 deletions docs/releases/server-1.6.521.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Server v1.6.521

Packages Engine `v0.183.337` and unchanged Web Console `1.6.181`.

## API Key audit identity

Rejected Key edits after request validation could be recorded under an internal
numeric Key ID, while the Key audit screen queried the public ID. The events
were stored but omitted from that query. Engine now consistently formats the
authoritative Key identity. The audit reader also recognizes historical numeric
IDs, but only as exact aliases of the Engine-authorized Key. Other resource IDs,
unknown ownership and inaccessible environments are not treated as aliases.
Stored events are not rewritten. Safe list/detail projections return public
Key IDs consistently, including pagination and outcome filters.

## Verification boundary

Focused tests cover new events, historical list/detail queries, both Key types,
pagination, filters, cross-Key and cross-environment exclusions, secret-field
projection, and unchanged MFA confirmation. Full DTO and database-row checks on
rejected policy edits are part of runtime acceptance. These checks are not a
claim that every unrelated resource/role combination has been exercised.

## Upgrade and rollback

Use `ghcr.io/pasturestack/server:v1.6.521`. Preserve existing environment variables,
volumes, networking, restart policy and security options. No database migration
or proxy/authentication configuration change is required.

`v1.6.520` is the compatible rollback version; it restores the numeric-ID audit
query defect. Follow the [API Key rollback guidance](../upgrades/api-key-policy-rollback.md)
before considering an older, policy-unaware release.
14 changes: 7 additions & 7 deletions server/Dockerfile.web-compose-release
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ ARG WEBHOOK_AUTOMATION_SERVICE_VERSION=0.10.4
ARG WEBHOOK_AUTOMATION_SERVICE_ARCHIVE_SHA256=49c4579829a04e758045fae02a5a9fca12bb0ba3af0d5e979cf9eb97f23a88a9
ARG WEBHOOK_AUTOMATION_SERVICE_BINARY_SHA256=98c7faea665b7eb95206b8c73a6f47d644c5d2d0eae53f274f6a15faf5205744
ARG ORCHESTRATION_ENGINE_RELEASE_BASE_URL=https://github.com/PastureStack/orchestration-engine/releases/download
ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.336
ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.337
ARG ORCHESTRATION_ENGINE_ARTIFACT=cattle.jar
ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=bd0dabf0bd87a859b97941657ae18e8dd551e390697494348c101782a508f136
ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=7e45189f2b57097efc7929c04ddf7767aebbe027d1b9f90023aa6711522c5c80
ARG COMPOSE_EXECUTOR_RELEASE_BASE_URL=https://github.com/PastureStack/compose-cli/releases/download
ARG COMPOSE_EXECUTOR_VERSION=0.14.37
ARG COMPOSE_EXECUTOR_ARCHIVE_SHA256=5ff465601930218f531885a384f231d969033e4cef794ce33d16bfe24e5c1c81
Expand Down Expand Up @@ -565,9 +565,9 @@ FROM ${BASE_IMAGE}

ARG SOURCE_DATE_EPOCH
ARG PASTURESTACK_SERVER_REVISION
ARG SERVER_RELEASE_TAG=v1.6.520
ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.336
ARG ORCHESTRATION_ENGINE_VERSION=0.183.336
ARG SERVER_RELEASE_TAG=v1.6.521
ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.337
ARG ORCHESTRATION_ENGINE_VERSION=0.183.337
ARG ENGINE_READONLY_SCHEMA_SHA256=7f274219e8dd9c6d750a408a6edec1448b564f4b16ba2204c425c5b45cec2233
ARG ENGINE_RESTRICTED_SCHEMA_SHA256=f854ba99260f29e324ab7446a8592996946038920ac9bcf17689d98d1ebc5e51
ARG HOST_API_VERSION=0.38.5
Expand Down Expand Up @@ -606,8 +606,8 @@ ARG WEBHOOK_AUTOMATION_SERVICE_VERSION=0.10.4
ARG WEBHOOK_AUTOMATION_SERVICE_COMMIT=400118b893843d2a7d7c65cc70c3449d76c4a8d8
ARG WEBHOOK_AUTOMATION_SERVICE_ARCHIVE_SHA256=49c4579829a04e758045fae02a5a9fca12bb0ba3af0d5e979cf9eb97f23a88a9
ARG WEBHOOK_AUTOMATION_SERVICE_BINARY_SHA256=98c7faea665b7eb95206b8c73a6f47d644c5d2d0eae53f274f6a15faf5205744
ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=bd0dabf0bd87a859b97941657ae18e8dd551e390697494348c101782a508f136
ARG ORCHESTRATION_ENGINE_COMMIT=1c7d4537becfd578d568582564b72e7c4d0b1e93
ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=7e45189f2b57097efc7929c04ddf7767aebbe027d1b9f90023aa6711522c5c80
ARG ORCHESTRATION_ENGINE_COMMIT=ba041fe312dfe97fc1e5144af576ddcf1b573d8e
ARG HOST_PROVISIONER_VERSION=0.39.8
ARG HOST_PROVISIONER_COMMIT=385e5b536c108f17fdfcdec84c01750a5be5ab1c
ARG HOST_PROVISIONER_ARCHIVE_SHA256=d775f36a613b1a486a5e60d6ad61fdbd1ebb4bd22422cbb6dcb70fdd7c4abf0c
Expand Down
10 changes: 5 additions & 5 deletions server/build-api-explorer-patch-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ if [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then
fi

revision=${PASTURESTACK_SERVER_REVISION:-$(git rev-parse HEAD)}
server_release_tag=${SERVER_RELEASE_TAG:-v1.6.520}
server_release_tag=${SERVER_RELEASE_TAG:-v1.6.521}
source_date_epoch=${SOURCE_DATE_EPOCH:-$(git show -s --format=%ct HEAD)}
base_image=${BASE_IMAGE:-ghcr.io/pasturestack/server:v1.6.460@sha256:c855af8aea232dacc5bb6df68e2271d482c68b53c43ab0c108ec19118f5ab403}
orchestration_engine_release_base_url=${ORCHESTRATION_ENGINE_RELEASE_BASE_URL:-https://github.com/PastureStack/orchestration-engine/releases/download}
orchestration_engine_release_tag=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.336}
orchestration_engine_release_tag=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.337}
orchestration_engine_artifact=${ORCHESTRATION_ENGINE_ARTIFACT:-cattle.jar}
orchestration_engine_artifact_sha256=${ORCHESTRATION_ENGINE_ARTIFACT_SHA256:-bd0dabf0bd87a859b97941657ae18e8dd551e390697494348c101782a508f136}
orchestration_engine_commit=${ORCHESTRATION_ENGINE_COMMIT:-1c7d4537becfd578d568582564b72e7c4d0b1e93}
orchestration_engine_artifact_sha256=${ORCHESTRATION_ENGINE_ARTIFACT_SHA256:-7e45189f2b57097efc7929c04ddf7767aebbe027d1b9f90023aa6711522c5c80}
orchestration_engine_commit=${ORCHESTRATION_ENGINE_COMMIT:-ba041fe312dfe97fc1e5144af576ddcf1b573d8e}
orchestration_engine_version=${orchestration_engine_release_tag#v}
engine_readonly_schema_sha256=${ENGINE_READONLY_SCHEMA_SHA256:-7f274219e8dd9c6d750a408a6edec1448b564f4b16ba2204c425c5b45cec2233}
engine_restricted_schema_sha256=${ENGINE_RESTRICTED_SCHEMA_SHA256:-f854ba99260f29e324ab7446a8592996946038920ac9bcf17689d98d1ebc5e51}
Expand Down Expand Up @@ -101,7 +101,7 @@ usage_telemetry_agent_archive_sha256=${USAGE_TELEMETRY_AGENT_ARCHIVE_SHA256:-5ce
usage_telemetry_agent_binary_sha256=${USAGE_TELEMETRY_AGENT_BINARY_SHA256:-e62a21270142181315293d7e11482288ffe8fc4d91cc8fe07dacc90b461f3709}
supported_docker_range='~v1.12.3 || ~v1.13.0 || ~v17.03.0 || ~v17.06.0 || ~v17.09.0 || ~v17.12.0 || ~v18.03.0 || ~v18.06.0 || ~v18.09.0 || ~v19.03.2 || v24.0.9 || >=v29.4.1 <=v29.7.2 || v29.8.0'
newest_docker_version=v29.8.0
image=${IMAGE:-pasturestack-validation/server:v1.6.520}
image=${IMAGE:-pasturestack-validation/server:v1.6.521}
build_options=()

[[ "$revision" =~ ^[0-9a-f]{40}$ ]]
Expand Down
4 changes: 2 additions & 2 deletions server/build-component-candidate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ for name in "${required[@]}"; do
case "$name" in *_COMMIT) [[ ${!name} =~ ^[0-9a-f]{40}$ ]] ;; *) [[ ${!name} =~ ^[0-9a-f]{64}$ ]] ;; esac
export "$name"
done
export SERVER_RELEASE_TAG=${SERVER_RELEASE_TAG:-v1.6.520}
export ORCHESTRATION_ENGINE_RELEASE_TAG=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.336}
export SERVER_RELEASE_TAG=${SERVER_RELEASE_TAG:-v1.6.521}
export ORCHESTRATION_ENGINE_RELEASE_TAG=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.337}
export ORCHESTRATION_ENGINE_ARTIFACT=cattle.jar
export WEB_CONSOLE_RELEASE_TAG=${WEB_CONSOLE_RELEASE_TAG:-1.6.181}
export WEB_CONSOLE_ARTIFACT="web-console-${WEB_CONSOLE_RELEASE_TAG}.tar.gz"
Expand Down
4 changes: 2 additions & 2 deletions server/console-broker/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ func (b *broker) runAuditQuery(ctx context.Context, incoming *http.Request, quer

scoped := make([]map[string]any, 0, len(records))
for _, record := range records {
if query.KeyID != "" && keyAuditMetadataString(record, "keyId") != query.KeyID {
if query.KeyID != "" && !keyAuditIDMatches(keyAuditMetadataString(record, "keyId"), query.KeyID) {
continue
}
projectID := auditString(record, "accountId")
Expand All @@ -395,7 +395,7 @@ func (b *broker) runAuditQuery(ctx context.Context, incoming *http.Request, quer

copyRecord := cloneAuditRecord(record)
if query.KeyID != "" {
copyRecord = safeKeyAuditRecord(record)
copyRecord = safeKeyAuditRecord(record, query.KeyID)
}
copyRecord["environmentDisplayName"] = allowedProjects[projectID]
actorID := auditString(record, "authenticatedAsAccountId")
Expand Down
26 changes: 25 additions & 1 deletion server/console-broker/keyaudit.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
const keyAuditQueryPath = "/v2-beta/pasturestack/key-audit-logs"

var keyAuditIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]{0,159}$`)
var canonicalKeyAuditIDPattern = regexp.MustCompile(`^1c([1-9][0-9]*)$`)

func (b *broker) fetchKeyAuditLogs(ctx context.Context, incoming *http.Request, query auditQuery,
allowedProjects map[string]string, baseValues url.Values) ([]map[string]any, error) {
Expand Down Expand Up @@ -249,7 +250,21 @@ func keyAuditMetadataString(record map[string]any, field string) string {
return ""
}

func safeKeyAuditRecord(record map[string]any) map[string]any {
// The Engine used internal numeric credential IDs for some governance failures.
// Match that historical form only against the current Engine-verified Key ID;
// unrelated typed IDs and records without keyId must not enter the Key view.
func keyAuditIDMatches(recordedID, keyID string) bool {
if keyID == "" {
return false
}
if recordedID == keyID {
return true
}
canonical := canonicalKeyAuditIDPattern.FindStringSubmatch(keyID)
return canonical != nil && recordedID == canonical[1]
}

func safeKeyAuditRecord(record map[string]any, keyID string) map[string]any {
result := make(map[string]any)
for _, field := range []string{
"type", "id", "created", "accountId", "authenticatedAsAccountId", "authenticatedAsIdentityId",
Expand All @@ -267,5 +282,14 @@ func safeKeyAuditRecord(record map[string]any) map[string]any {
result[field] = value
}
}
if keyAuditIDMatches(keyAuditMetadataString(record, "keyId"), keyID) {
result["keyId"] = keyID
}
switch strings.ToLower(keyAuditMetadataString(record, "targetType")) {
case "apikey", "apikeyrestricted":
if keyAuditIDMatches(keyAuditMetadataString(record, "targetId"), keyID) {
result["targetId"] = keyID
}
}
return result
}
131 changes: 128 additions & 3 deletions server/console-broker/keyaudit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

type keyAuditFixture struct {
mu sync.Mutex
keyID string
owner string
keyStatus int
restricted bool
Expand Down Expand Up @@ -44,7 +45,7 @@ type keyAuditFixture struct {
func newKeyAuditFixture(t *testing.T) (*keyAuditFixture, *httptest.Server) {
t.Helper()
fixture := &keyAuditFixture{
owner: "1a1", viewer: "1a1", keyStatus: http.StatusOK,
keyID: "1a99", owner: "1a1", viewer: "1a1", keyStatus: http.StatusOK,
projects: []map[string]any{{"type": "project", "id": "1p1", "name": "Production"}},
}
upstream := &auditUpstreamFixture{}
Expand All @@ -57,7 +58,7 @@ func newKeyAuditFixture(t *testing.T) (*keyAuditFixture, *httptest.Server) {
defer fixture.mu.Unlock()
writer.Header().Set("X-API-USER-ID", fixture.viewer)
switch request.URL.Path {
case "/v2-beta/apikey/1a99", "/v2-beta/apikeyrestricted/1a99":
case "/v2-beta/apikey/" + fixture.keyID, "/v2-beta/apikeyrestricted/" + fixture.keyID:
fixture.keyQueries = append(fixture.keyQueries, request.URL.Query())
fixture.keyProjectHeaders = append(fixture.keyProjectHeaders, request.Header.Get("X-API-Project-ID"))
kind := "apiKey"
Expand Down Expand Up @@ -87,7 +88,7 @@ func newKeyAuditFixture(t *testing.T) (*keyAuditFixture, *httptest.Server) {
return
}
}
key := map[string]any{"type": kind, "id": "1a99", "accountId": fixture.owner, "secretValue": "never-return-key-secret"}
key := map[string]any{"type": kind, "id": fixture.keyID, "accountId": fixture.owner, "secretValue": "never-return-key-secret"}
for field, value := range fixture.scopedKeyOverride {
switch field {
case "viewer":
Expand Down Expand Up @@ -185,6 +186,130 @@ func keyAuditRecord(id, keyID, project, created string) map[string]any {
return record
}

func TestKeyAuditIDMatchesOnlyExactOrCanonicalCredentialAlias(t *testing.T) {
for _, tc := range []struct {
name, recorded, key string
match bool
}{
{"typed", "1c99", "1c99", true},
{"legacy", "99", "1c99", true},
{"existing-exact-contract", "1a99", "1a99", true},
{"different-key", "98", "1c99", false},
{"different-type", "1a99", "1c99", false},
{"non-credential-query", "99", "1a99", false},
{"suffix-only", "prefix99", "1c99", false},
{"leading-zero", "099", "1c99", false},
{"noncanonical-query", "99", "1c099", false},
{"missing-key", "", "1c99", false},
{"empty-query", "", "", false},
} {
t.Run(tc.name, func(t *testing.T) {
if got := keyAuditIDMatches(tc.recorded, tc.key); got != tc.match {
t.Fatalf("key match = %v, want %v", got, tc.match)
}
})
}
}

func TestSafeKeyAuditCanonicalizesOnlyMatchingKeyTargets(t *testing.T) {
for _, tc := range []struct{ kind, target, expected string }{
{"apiKey", "99", "1c99"},
{"apiKeyRestricted", "99", "1c99"},
{"apiKey", "1c99", "1c99"},
{"apiKey", "98", "98"},
{"apiKey", "1a99", "1a99"},
{"stack", "99", "99"},
{"container", "99", "99"},
{"", "99", "99"},
} {
t.Run(tc.kind+"/"+tc.target, func(t *testing.T) {
record := map[string]any{"keyId": "99", "targetType": tc.kind, "targetId": tc.target,
"requestObject": map[string]any{"secretValue": "must-not-leak"}}
result := safeKeyAuditRecord(record, "1c99")
if result["keyId"] != "1c99" || result["targetId"] != tc.expected {
t.Fatalf("incorrect canonical metadata: %#v", result)
}
if record["keyId"] != "99" || record["targetId"] != tc.target {
t.Fatal("canonical projection mutated the historical source record")
}
if _, present := result["requestObject"]; present {
t.Fatal("historical compatibility bypassed the safe projection")
}
})
}
}

func TestKeyAuditHistoricalNumericIdentifiersBeforeCountDetailAndFilters(t *testing.T) {
for _, restricted := range []bool{false, true} {
for _, nested := range []bool{false, true} {
t.Run("restricted="+strconv.FormatBool(restricted)+"/nested="+strconv.FormatBool(nested), func(t *testing.T) {
fixture, server := newKeyAuditFixture(t)
fixture.keyID, fixture.restricted, fixture.accountScoped = "1c99", restricted, true
kind := "apiKey"
if restricted {
kind = "apiKeyRestricted"
}
legacy := keyAuditRecord("legacy", "99", "1a1", "2021-01-01T00:00:00Z")
legacy["targetType"], legacy["targetId"], legacy["eventType"] = kind, "99", "api.apiKey.update"
legacy["outcome"], legacy["httpStatus"] = "FAILED", 400
if nested {
fields := make(map[string]any)
for _, field := range []string{"keyId", "targetType", "targetId", "outcome", "httpStatus"} {
fields[field] = legacy[field]
delete(legacy, field)
}
legacy["data"] = map[string]any{"fields": fields, "secretValue": "nested-must-not-leak"}
}
denied := keyAuditRecord("denied", "99", "1p1", "2022-01-01T00:00:00Z")
denied["targetType"], denied["targetId"], denied["outcome"], denied["httpStatus"] = kind, "99", "DENIED", 403
missing := keyAuditRecord("missing-key", "", "1a1", "2023-01-01T00:00:00Z")
missing["resourceId"] = "1c99" // Resource IDs cannot substitute for missing Key attribution.
fixture.records = []map[string]any{
keyAuditRecord("modern", "1c99", "1a1", "2020-01-01T00:00:00Z"), legacy, denied,
keyAuditRecord("foreign-key", "98", "1a1", "2023-01-01T00:00:00Z"),
keyAuditRecord("foreign-type", "1a99", "1a1", "2023-01-01T00:00:00Z"),
keyAuditRecord("hidden-project", "99", "1p9", "2023-01-01T00:00:00Z"), missing,
}
response := performAuditRequest(t, server, keyAuditQueryPath+"?keyId=1c99&timeScope=all&limit=1&offset=1&order=asc")
var payload auditCollection
err := json.NewDecoder(response.Body).Decode(&payload)
response.Body.Close()
if response.StatusCode != http.StatusOK || err != nil || payload.Pagination["total"] != float64(3) || len(payload.Data) != 1 ||
auditString(payload.Data[0], "id") != "legacy" || auditString(payload.Data[0], "keyId") != "1c99" ||
auditString(payload.Data[0], "targetId") != "1c99" {
t.Fatalf("historical count/pagination/canonical metadata lost: status=%d err=%v payload=%#v", response.StatusCode, err, payload)
}
detail := performAuditRequest(t, server, keyAuditQueryPath+"/legacy?keyId=1c99")
var record map[string]any
err = json.NewDecoder(detail.Body).Decode(&record)
detail.Body.Close()
if detail.StatusCode != http.StatusOK || err != nil || auditString(record, "keyId") != "1c99" || auditString(record, "targetId") != "1c99" {
t.Fatalf("historical detail unavailable: status=%d err=%v record=%#v", detail.StatusCode, err, record)
}
serialized, _ := json.Marshal(record)
for _, secret := range []string{"must-not-leak", "Bearer-secret", "session-secret", "jwt-secret", "code-secret", "never-return-key-secret"} {
if strings.Contains(string(serialized), secret) {
t.Fatal("historical detail exposed a secret")
}
}
filtered := performAuditRequest(t, server, keyAuditQueryPath+"?keyId=1c99&timeScope=all&outcome=DENIED")
err = json.NewDecoder(filtered.Body).Decode(&payload)
filtered.Body.Close()
if filtered.StatusCode != http.StatusOK || err != nil || payload.Pagination["total"] != float64(1) || len(payload.Data) != 1 || auditString(payload.Data[0], "id") != "denied" {
t.Fatalf("historical denial filter lost: status=%d err=%v payload=%#v", filtered.StatusCode, err, payload)
}
for _, id := range []string{"foreign-key", "foreign-type", "hidden-project", "missing-key"} {
hidden := performAuditRequest(t, server, keyAuditQueryPath+"/"+id+"?keyId=1c99")
hidden.Body.Close()
if hidden.StatusCode != http.StatusNotFound {
t.Fatalf("historical alias admitted unrelated event %s: %d", id, hidden.StatusCode)
}
}
})
}
}
}

func TestKeyAuditFiltersBeforeCountAndPaginationAndAllTimeIncludesOldReads(t *testing.T) {
fixture, server := newKeyAuditFixture(t)
fixture.records = []map[string]any{
Expand Down
2 changes: 1 addition & 1 deletion server/security/openvex.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "https://github.com/PastureStack/server/security/openvex/v1.6.520",
"@id": "https://github.com/PastureStack/server/security/openvex/v1.6.521",
"author": "PastureStack Security",
"timestamp": "2026-10-02T22:05:00Z",
"version": 51,
Expand Down
Loading