Fix OpenAPI spec parsing errors in Swagger UI

- Replace type: [string, "null"] array syntax with
  type: string + nullable: true on AuditEvent.actor_id,
  AuditEvent.target_id, PolicyRule.not_before, and
  PolicyRule.expires_at; Swagger UI 5 cannot parse the
  JSON Schema array form
- Add missing username field to /v1/token/validate response
  schema (added to handler in d6cc827 but never synced)
- Add missing GET /v1/pgcreds endpoint to spec
- Sync web/static/openapi.yaml (served file) with root;
  the static copy was many commits out of date, missing
  all policy/tags schemas and endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-15 16:29:53 -07:00
parent d4e8ef90ee
commit 9657f18784
2 changed files with 838 additions and 89 deletions

View File

@@ -100,13 +100,15 @@ components:
format: date-time
example: "2026-03-11T09:01:23Z"
actor_id:
type: [string, "null"]
type: string
format: uuid
nullable: true
description: UUID of the account that performed the action. Null for bootstrap events.
example: 550e8400-e29b-41d4-a716-446655440000
target_id:
type: [string, "null"]
type: string
format: uuid
nullable: true
description: UUID of the affected account, if applicable.
ip_address:
type: string
@@ -205,16 +207,18 @@ components:
type: boolean
example: true
not_before:
type: [string, "null"]
type: string
format: date-time
nullable: true
description: |
Earliest time the rule becomes active. NULL means no constraint
(always active). Rules where `not_before > now()` are skipped
during evaluation.
example: "2026-04-01T00:00:00Z"
expires_at:
type: [string, "null"]
type: string
format: date-time
nullable: true
description: |
Time after which the rule is no longer active. NULL means no
constraint (never expires). Rules where expires_at is in the past
@@ -602,6 +606,10 @@ paths:
format: uuid
description: Subject (account UUID). Present when valid=true.
example: 550e8400-e29b-41d4-a716-446655440000
username:
type: string
description: Account username. Present when valid=true and the account exists.
example: alice
roles:
type: array
items:
@@ -615,7 +623,7 @@ paths:
example: "2026-04-10T12:34:56Z"
examples:
valid:
value: {valid: true, sub: "550e8400-...", roles: [editor], expires_at: "2026-04-10T12:34:56Z"}
value: {valid: true, sub: "550e8400-...", username: alice, roles: [editor], expires_at: "2026-04-10T12:34:56Z"}
invalid:
value: {valid: false}
"429":
@@ -1263,6 +1271,70 @@ paths:
"404":
$ref: "#/components/responses/NotFound"
/v1/pgcreds:
get:
summary: List accessible Postgres credentials
description: |
Return all Postgres credentials accessible to the authenticated account:
credentials owned by the account plus any explicitly granted by an admin.
The `id` field is the credential record ID; use it together with the
`service_account_id` to fetch full details via
`GET /v1/accounts/{id}/pgcreds`. Passwords are **not** returned by this
endpoint.
operationId: listAccessiblePGCreds
tags: [Admin — Credentials]
security:
- bearerAuth: []
responses:
"200":
description: Array of accessible Postgres credential summaries.
content:
application/json:
schema:
type: array
items:
type: object
required: [id, service_account_id, host, port, database, username, created_at, updated_at]
properties:
id:
type: integer
description: Credential record ID.
example: 7
service_account_id:
type: string
format: uuid
description: UUID of the system account that owns these credentials.
example: 550e8400-e29b-41d4-a716-446655440000
service_account_name:
type: string
description: Username of the owning system account (omitted if unavailable).
example: payments-api
host:
type: string
example: db.example.com
port:
type: integer
example: 5432
database:
type: string
example: mydb
username:
type: string
example: myuser
created_at:
type: string
format: date-time
example: "2026-03-11T09:00:00Z"
updated_at:
type: string
format: date-time
example: "2026-03-11T09:00:00Z"
"401":
$ref: "#/components/responses/Unauthorized"
"503":
$ref: "#/components/responses/VaultSealed"
/v1/audit:
get:
summary: Query audit log (admin)

View File

@@ -118,6 +118,121 @@ components:
description: JSON blob with event-specific metadata. Never contains credentials.
example: '{"jti":"f47ac10b-..."}'
TagsResponse:
type: object
required: [tags]
properties:
tags:
type: array
items:
type: string
description: Current tag list for the account.
example: ["env:production", "svc:payments-api"]
RuleBody:
type: object
required: [effect]
description: |
The match conditions and effect of a policy rule. All fields except
`effect` are optional; an omitted field acts as a wildcard.
properties:
effect:
type: string
enum: [allow, deny]
example: allow
roles:
type: array
items:
type: string
description: Subject must have at least one of these roles.
example: ["svc:payments-api"]
account_types:
type: array
items:
type: string
enum: [human, system]
description: Subject account type must be one of these.
example: ["system"]
subject_uuid:
type: string
format: uuid
description: Match only this specific subject UUID.
example: 550e8400-e29b-41d4-a716-446655440000
actions:
type: array
items:
type: string
description: |
One of the defined action constants, e.g. `pgcreds:read`,
`accounts:list`. Subject action must be in this list.
example: ["pgcreds:read"]
resource_type:
type: string
description: Resource type the rule applies to.
example: pgcreds
owner_matches_subject:
type: boolean
description: Resource owner UUID must equal the subject UUID.
example: true
service_names:
type: array
items:
type: string
description: Resource service name must be one of these.
example: ["payments-api"]
required_tags:
type: array
items:
type: string
description: Resource must have ALL of these tags.
example: ["env:staging"]
PolicyRule:
type: object
required: [id, priority, description, rule, enabled, created_at, updated_at]
properties:
id:
type: integer
example: 1
priority:
type: integer
description: Lower number = evaluated first.
example: 100
description:
type: string
example: Allow payments-api to read its own pgcreds
rule:
$ref: "#/components/schemas/RuleBody"
enabled:
type: boolean
example: true
not_before:
type: string
format: date-time
nullable: true
description: |
Earliest time the rule becomes active. NULL means no constraint
(always active). Rules where `not_before > now()` are skipped
during evaluation.
example: "2026-04-01T00:00:00Z"
expires_at:
type: string
format: date-time
nullable: true
description: |
Time after which the rule is no longer active. NULL means no
constraint (never expires). Rules where expires_at is in the past
are skipped during evaluation.
example: "2026-06-01T00:00:00Z"
created_at:
type: string
format: date-time
example: "2026-03-11T09:00:00Z"
updated_at:
type: string
format: date-time
example: "2026-03-11T09:00:00Z"
PGCreds:
type: object
required: [host, port, database, username, password]
@@ -192,6 +307,18 @@ components:
error: rate limit exceeded
code: rate_limited
VaultSealed:
description: |
The vault is sealed. The server is running but has no key material.
Unseal via `POST /v1/vault/unseal` before retrying.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error: vault is sealed
code: vault_sealed
paths:
# ── Public ────────────────────────────────────────────────────────────────
@@ -200,14 +327,16 @@ paths:
get:
summary: Health check
description: |
Returns `{"status":"ok"}` if the server is running and the vault is
unsealed, or `{"status":"sealed"}` if the vault is sealed.
No auth required.
Returns server health status. Always returns HTTP 200, even when the
vault is sealed. No auth required.
When the vault is sealed, `status` is `"sealed"` and most other
endpoints return 503. When healthy, `status` is `"ok"`.
operationId: getHealth
tags: [Public]
responses:
"200":
description: Server is healthy (may be sealed).
description: Server is running (check `status` for sealed state).
content:
application/json:
schema:
@@ -218,84 +347,6 @@ paths:
enum: [ok, sealed]
example: ok
/v1/vault/status:
get:
summary: Vault seal status
description: Returns `{"sealed": true}` or `{"sealed": false}`. No auth required.
operationId: getVaultStatus
tags: [Vault]
responses:
"200":
description: Current seal state.
content:
application/json:
schema:
type: object
properties:
sealed:
type: boolean
/v1/vault/unseal:
post:
summary: Unseal the vault
description: |
Accepts a passphrase, derives the master key, and unseals the vault.
Rate-limited to 3 requests per second, burst of 5.
No auth required (the vault is sealed, so no tokens can be validated).
operationId: unsealVault
tags: [Vault]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [passphrase]
properties:
passphrase:
type: string
description: Master passphrase for key derivation.
responses:
"200":
description: Vault unsealed successfully.
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: unsealed
"401":
description: Unseal failed (wrong passphrase).
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/vault/seal:
post:
summary: Seal the vault
description: |
Seals the vault, zeroing all key material in memory.
Requires admin authentication. The caller's token becomes invalid
after sealing.
operationId: sealVault
tags: [Vault]
security:
- bearerAuth: []
responses:
"200":
description: Vault sealed successfully.
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: sealed
/v1/keys/public:
get:
summary: Ed25519 public key (JWK)
@@ -336,6 +387,121 @@ paths:
description: Base64url-encoded public key bytes.
example: 11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo
/v1/vault/status:
get:
summary: Vault seal status
description: |
Returns whether the vault is currently sealed. Always accessible,
even when sealed. No auth required.
Clients should poll this after startup or after a 503 `vault_sealed`
response to determine when to attempt an unseal.
operationId: getVaultStatus
tags: [Public]
responses:
"200":
description: Current vault seal state.
content:
application/json:
schema:
type: object
required: [sealed]
properties:
sealed:
type: boolean
example: false
/v1/vault/unseal:
post:
summary: Unseal the vault
description: |
Provide the master passphrase to derive the encryption key, decrypt
the Ed25519 signing key, and unseal the vault. Once unsealed, all
other endpoints become available.
Rate limited to 3 requests per second per IP (burst 5) to limit
brute-force attempts against the passphrase.
The passphrase is never logged. A generic `"unseal failed"` error
is returned for any failure (wrong passphrase, vault already unsealed
mid-flight, etc.) to avoid leaking information.
operationId: unsealVault
tags: [Public]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [passphrase]
properties:
passphrase:
type: string
description: Master passphrase used to derive the encryption key.
example: correct-horse-battery-staple
responses:
"200":
description: Vault unsealed (or was already unsealed).
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: [unsealed, already unsealed]
example: unsealed
"400":
$ref: "#/components/responses/BadRequest"
"401":
description: Wrong passphrase or key decryption failure.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error: unseal failed
code: unauthorized
"429":
$ref: "#/components/responses/RateLimited"
/v1/vault/seal:
post:
summary: Seal the vault (admin)
description: |
Zero all key material in memory and transition the server to the
sealed state. After this call:
- All subsequent requests (except health, vault status, and unseal)
return 503 `vault_sealed`.
- The caller's own JWT is immediately invalidated because the public
key needed to verify it is no longer held in memory.
- The server can be unsealed again via `POST /v1/vault/unseal`.
This is an emergency operation. Use it to protect key material if a
compromise is suspected. It does **not** restart the server or wipe
the database.
operationId: sealVault
tags: [Admin — Vault]
security:
- bearerAuth: []
responses:
"200":
description: Vault sealed (or was already sealed).
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: [sealed, already sealed]
example: sealed
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
/v1/auth/login:
post:
summary: Login
@@ -440,6 +606,10 @@ paths:
format: uuid
description: Subject (account UUID). Present when valid=true.
example: 550e8400-e29b-41d4-a716-446655440000
username:
type: string
description: Account username. Present when valid=true and the account exists.
example: alice
roles:
type: array
items:
@@ -453,7 +623,7 @@ paths:
example: "2026-04-10T12:34:56Z"
examples:
valid:
value: {valid: true, sub: "550e8400-...", roles: [editor], expires_at: "2026-04-10T12:34:56Z"}
value: {valid: true, sub: "550e8400-...", username: alice, roles: [editor], expires_at: "2026-04-10T12:34:56Z"}
invalid:
value: {valid: false}
"429":
@@ -578,6 +748,68 @@ paths:
"401":
$ref: "#/components/responses/Unauthorized"
/v1/auth/password:
put:
summary: Change own password (self-service)
description: |
Change the password of the currently authenticated human account.
The caller must supply the correct `current_password` to prevent
token-theft attacks: possession of a valid JWT alone is not sufficient.
On success:
- The stored Argon2id hash is replaced with the new password hash.
- All active sessions *except* the caller's current token are revoked.
- The lockout failure counter is cleared.
On failure (wrong current password):
- A login failure is recorded against the account, subject to the
same lockout rules as `POST /v1/auth/login`.
Only applies to human accounts. System accounts have no password.
operationId: changePassword
tags: [Auth]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [current_password, new_password]
properties:
current_password:
type: string
description: The account's current password (required for verification).
example: old-s3cr3t
new_password:
type: string
description: The new password. Minimum 12 characters.
example: new-s3cr3t-long
responses:
"204":
description: Password changed. Other active sessions revoked.
"400":
$ref: "#/components/responses/BadRequest"
"401":
description: Current password is incorrect.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error: current password is incorrect
code: unauthorized
"429":
description: Account temporarily locked due to too many failed attempts.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error: account temporarily locked
code: account_locked
# ── Admin ──────────────────────────────────────────────────────────────────
/v1/auth/totp:
@@ -911,6 +1143,76 @@ paths:
"404":
$ref: "#/components/responses/NotFound"
post:
summary: Grant a role to an account (admin)
description: |
Add a single role to an account's role set. If the role already exists,
this is a no-op. Roles take effect in the **next** token issued or
renewed; existing tokens continue to carry the roles embedded at
issuance time.
operationId: grantRole
tags: [Admin — Accounts]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [role]
properties:
role:
type: string
example: editor
responses:
"204":
description: Role granted.
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
/v1/accounts/{id}/roles/{role}:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
example: 550e8400-e29b-41d4-a716-446655440000
- name: role
in: path
required: true
schema:
type: string
example: editor
delete:
summary: Revoke a role from an account (admin)
description: |
Remove a single role from an account's role set. Roles take effect in
the **next** token issued or renewed; existing tokens continue to carry
the roles embedded at issuance time.
operationId: revokeRole
tags: [Admin — Accounts]
security:
- bearerAuth: []
responses:
"204":
description: Role revoked.
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
/v1/accounts/{id}/pgcreds:
parameters:
- name: id
@@ -969,6 +1271,70 @@ paths:
"404":
$ref: "#/components/responses/NotFound"
/v1/pgcreds:
get:
summary: List accessible Postgres credentials
description: |
Return all Postgres credentials accessible to the authenticated account:
credentials owned by the account plus any explicitly granted by an admin.
The `id` field is the credential record ID; use it together with the
`service_account_id` to fetch full details via
`GET /v1/accounts/{id}/pgcreds`. Passwords are **not** returned by this
endpoint.
operationId: listAccessiblePGCreds
tags: [Admin — Credentials]
security:
- bearerAuth: []
responses:
"200":
description: Array of accessible Postgres credential summaries.
content:
application/json:
schema:
type: array
items:
type: object
required: [id, service_account_id, host, port, database, username, created_at, updated_at]
properties:
id:
type: integer
description: Credential record ID.
example: 7
service_account_id:
type: string
format: uuid
description: UUID of the system account that owns these credentials.
example: 550e8400-e29b-41d4-a716-446655440000
service_account_name:
type: string
description: Username of the owning system account (omitted if unavailable).
example: payments-api
host:
type: string
example: db.example.com
port:
type: integer
example: 5432
database:
type: string
example: mydb
username:
type: string
example: myuser
created_at:
type: string
format: date-time
example: "2026-03-11T09:00:00Z"
updated_at:
type: string
format: date-time
example: "2026-03-11T09:00:00Z"
"401":
$ref: "#/components/responses/Unauthorized"
"503":
$ref: "#/components/responses/VaultSealed"
/v1/audit:
get:
summary: Query audit log (admin)
@@ -980,7 +1346,10 @@ paths:
`token_issued`, `token_renewed`, `token_revoked`, `token_expired`,
`account_created`, `account_updated`, `account_deleted`,
`role_granted`, `role_revoked`, `totp_enrolled`, `totp_removed`,
`pgcred_accessed`, `pgcred_updated`.
`pgcred_accessed`, `pgcred_updated`, `pgcred_access_granted`,
`pgcred_access_revoked`, `tag_added`, `tag_removed`,
`policy_rule_created`, `policy_rule_updated`, `policy_rule_deleted`,
`policy_deny`, `vault_sealed`, `vault_unsealed`.
operationId: listAudit
tags: [Admin — Audit]
security:
@@ -1041,6 +1410,310 @@ paths:
"403":
$ref: "#/components/responses/Forbidden"
/v1/accounts/{id}/tags:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
example: 550e8400-e29b-41d4-a716-446655440000
get:
summary: Get account tags (admin)
description: |
Return the current tag set for an account. Tags are used by the policy
engine for machine/service gating (e.g. `env:production`,
`svc:payments-api`).
operationId: getAccountTags
tags: [Admin — Policy]
security:
- bearerAuth: []
responses:
"200":
description: Tag list.
content:
application/json:
schema:
$ref: "#/components/schemas/TagsResponse"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
put:
summary: Set account tags (admin)
description: |
Replace the account's full tag set atomically. Pass an empty array to
clear all tags. Changes take effect immediately for new policy
evaluations; no token renewal is required.
operationId: setAccountTags
tags: [Admin — Policy]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [tags]
properties:
tags:
type: array
items:
type: string
example: ["env:production", "svc:payments-api"]
responses:
"200":
description: Updated tag list.
content:
application/json:
schema:
$ref: "#/components/schemas/TagsResponse"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
/v1/accounts/{id}/password:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
example: 550e8400-e29b-41d4-a716-446655440000
put:
summary: Admin password reset (admin)
description: |
Reset the password for a human account without requiring the current
password. This is intended for account recovery (e.g. a user forgot
their password).
On success:
- The stored Argon2id hash is replaced with the new password hash.
- All active sessions for the target account are revoked.
Only applies to human accounts. The new password must be at least
12 characters.
operationId: adminSetPassword
tags: [Admin — Accounts]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [new_password]
properties:
new_password:
type: string
description: The new password. Minimum 12 characters.
example: new-s3cr3t-long
responses:
"204":
description: Password reset. All active sessions for the account revoked.
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
/v1/policy/rules:
get:
summary: List policy rules (admin)
description: |
Return all operator-defined policy rules ordered by priority (ascending).
Built-in default rules (IDs -1 to -7) are not included.
operationId: listPolicyRules
tags: [Admin — Policy]
security:
- bearerAuth: []
responses:
"200":
description: Array of policy rules.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/PolicyRule"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
post:
summary: Create policy rule (admin)
description: |
Create a new operator policy rule. Rules are evaluated in priority order
(lower number = evaluated first, default 100). Deny-wins: if any matching
rule has effect `deny`, access is denied regardless of allow rules.
operationId: createPolicyRule
tags: [Admin — Policy]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [description, rule]
properties:
description:
type: string
example: Allow payments-api to read its own pgcreds
priority:
type: integer
description: Evaluation priority. Lower = first. Default 100.
example: 50
rule:
$ref: "#/components/schemas/RuleBody"
not_before:
type: string
format: date-time
description: Earliest activation time (RFC3339, optional).
example: "2026-04-01T00:00:00Z"
expires_at:
type: string
format: date-time
description: Expiry time (RFC3339, optional).
example: "2026-06-01T00:00:00Z"
responses:
"201":
description: Rule created.
content:
application/json:
schema:
$ref: "#/components/schemas/PolicyRule"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
/v1/policy/rules/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: integer
example: 1
get:
summary: Get policy rule (admin)
operationId: getPolicyRule
tags: [Admin — Policy]
security:
- bearerAuth: []
responses:
"200":
description: Policy rule.
content:
application/json:
schema:
$ref: "#/components/schemas/PolicyRule"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
patch:
summary: Update policy rule (admin)
description: |
Update one or more fields of an existing policy rule. All fields are
optional; omitted fields are left unchanged.
operationId: updatePolicyRule
tags: [Admin — Policy]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
description:
type: string
example: Updated description
priority:
type: integer
example: 75
enabled:
type: boolean
example: false
rule:
$ref: "#/components/schemas/RuleBody"
not_before:
type: string
format: date-time
description: Set earliest activation time (RFC3339).
example: "2026-04-01T00:00:00Z"
expires_at:
type: string
format: date-time
description: Set expiry time (RFC3339).
example: "2026-06-01T00:00:00Z"
clear_not_before:
type: boolean
description: Set to true to remove not_before constraint.
clear_expires_at:
type: boolean
description: Set to true to remove expires_at constraint.
responses:
"200":
description: Updated rule.
content:
application/json:
schema:
$ref: "#/components/schemas/PolicyRule"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
delete:
summary: Delete policy rule (admin)
description: Permanently delete a policy rule. This action cannot be undone.
operationId: deletePolicyRule
tags: [Admin — Policy]
security:
- bearerAuth: []
responses:
"204":
description: Rule deleted.
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
tags:
- name: Public
description: No authentication required.
@@ -1056,3 +1729,7 @@ tags:
description: Requires admin role.
- name: Admin — Audit
description: Requires admin role.
- name: Admin — Policy
description: Requires admin role. Manage policy rules and account tags.
- name: Admin — Vault
description: Requires admin role. Emergency vault seal operation.