fix(cabana): safe account action URLs, per-section isolation and the prompted record action contract (B-WR-03, B-WR-05, B-WR-06)

- an account action URL is kept only when it is http(s) with a host or a
  same-origin path; anything else is dropped and logged without the URL
- a failing Status marks its section error: true and a failing Applies
  drops only that action, instead of a 500 for the whole list
- status text goes through localizeDetail (B-IN-03); the non-string
  payload refusal is the phrase key action_prompt_invalid (B-IN-02)
- the record action annotations and pact's prompt doc say what happens
  (B-IN-05 text); admin.json and schema.d.ts regenerated
This commit is contained in:
Jakub Zych
2026-10-23 16:24:34 +02:00
parent 24cf6cd931
commit 44835e4c6b
12 changed files with 80 additions and 29 deletions

View File

@@ -2636,7 +2636,7 @@ export interface paths {
put?: never;
/**
* Run a toolbar action
* @description Runs a controller-registered action that the list's toolbar.buttons declares. The body must be {}: record_id, values and payload are all refused, and the answer's fill is always empty.
* @description Runs a controller-registered action that the list's toolbar.buttons declares. The body must be {}: the route refuses record_id, values and payload, and the answer's fill is always empty.
*/
post: {
parameters: {
@@ -3030,7 +3030,7 @@ export interface paths {
put?: never;
/**
* Run a declared record action
* @description Runs a record action the controller registers and the form's recordActions declares. The record is loaded and row-locked through the controller's form scope in one transaction (404 when missing or out of scope); an action that does not apply to the record's current state answers 409. The body must be {}: record_id, values and payload are all refused, and the answer's fill is always empty.
* @description Runs a record action the controller registers and the form's recordActions declares. The record is loaded and row-locked through the controller's form scope in one transaction (404 when missing or out of scope); an action that does not apply to the record's current state answers 409. The body is {} for an action without a prompt; an action whose record response carries prompt takes {"payload": "<text>"}, trimmed, non-empty and at most prompt.max_length runes, with a 422 on payload otherwise. record_id and values are always refused, and the answer's fill is always empty.
*/
post: {
parameters: {
@@ -3050,7 +3050,7 @@ export interface paths {
};
cookie?: never;
};
/** @description Empty object */
/** @description Empty object, or {payload: text} for a prompted action */
requestBody: {
content: {
"application/json": components["schemas"]["cabana.AdminActionRequest"];
@@ -5521,11 +5521,17 @@ export interface components {
"cabana.AccountContributionView": {
actions: components["schemas"]["cabana.AccountContributionActionView"][];
description: string;
/**
* @description Error is true when the section's status could not be loaded (the
* plugin's Status failed); the other sections are answered as usual.
* Absent otherwise.
*/
error?: boolean;
label: string;
name: string;
/**
* @description Status is the section's line for the signed-in admin; "" when the
* section has none.
* section has none or its status could not be loaded.
*/
status: string;
};
@@ -5577,7 +5583,8 @@ export interface components {
/**
* @description Payload is the widget's own JSON value (the summer-action event's
* detail.payload): any JSON, at most 64 KiB, handed to the action as-is.
* Toolbar and record routes refuse it.
* Toolbar routes refuse it; record routes accept it only for a prompted
* action, as a JSON string (the prompt's answer).
*/
payload?: unknown;
record_id?: number;