feat(secrets): add optional descriptions to workspace secrets (#6796)

* feat(secrets): add optional descriptions to workspace secrets

Workspace secrets already have a backing credential row with a description
column, but nothing surfaced it. Teammates had no way to record what a
secret is for.

- Add a Description field to the secret detail page, matching the
  integrations credential page, gated on workspace-secret admin
- Fold the value and description editors into one Save/Discard pair and
  one unsaved-changes guard; two guards cannot coexist, since each seeds
  its own same-URL history entry
- Match descriptions in the secrets settings search
- Expose description on GET/PUT /api/v2/secrets and in the CLI

Descriptions are workspace-only: env_personal credential rows are
per-workspace mirrors of one user-global secret, so one saved there would
exist in a single workspace, and a personal secret has no teammates to
inform. The API rejects a description on personal scope rather than
silently dropping it, and omitting it on PUT leaves any existing
description untouched so a value rotation cannot erase it.

* fix(secrets): address review findings on secret descriptions

- Patch the credential detail cache optimistically on update. `onMutate`
  cancelled the detail query but only patched the lists, so a detail-backed
  editor stayed dirty after a successful save until the refetch landed —
  long enough for Discard to restore the pre-save value over the committed
  one, and for Back to open the unsaved-changes guard.
- Memoize `useSecretValue`'s returned callbacks and object, per the hook
  convention, so the composed form's save/discard stop churning per render.
- Reject a description on a personal secret in the domain layer rather than
  only at the v2 boundary. The internal credential update path accepted one
  for any type, writing data every reader hides.
- Normalize an empty description to null so the API and UI agree.
- Correct the secrets documentation, which described a Display Name field
  the detail view does not have and omitted the scope rule.
- Drop the CLI's copy of the 500-character bound; it can't import the
  contract, so a copy only drifts from the message the API already returns.
- Collapse a redundant save guard and align the description write gate with
  the render gate.

Leaves the integrations credential page byte-identical to staging.

* fix(secrets): keep the API docs example and CLI column order stable

Backward-compatibility fixes for anyone who never sets a description.

- Move the blank-to-null normalization out of the contract and into the
  route. A Zod `.transform()` on any property drops the whole request
  schema's OpenAPI examples, which had silently removed the Set Secret
  request example from the published docs.
- Append the CLI `description` column instead of inserting it before
  `updated`. `--output text` is positional, so inserting would shift every
  field an existing script cuts.
- Reject a description on a personal secret with a message that says so,
  rather than dropping the field and falling through to the generic
  "no updatable fields" error.
This commit is contained in:
Waleed
2026-08-17 17:33:35 -07:00
committed by GitHub
parent 3abac09dc3
commit 0b4d34137b
24 changed files with 574 additions and 63 deletions
@@ -1787,6 +1787,7 @@ sim secrets set <name> [options]
| --- | --- | --- |
| `--scope <scope>` | Yes | Secret ownership scope. Accepted values: `workspace`, `personal`. |
| `--value <value>` | No | Secret value; visible to shell history when supplied directly. |
| `--description <description>` | No | What the secret is for, shown to teammates; workspace scope only. Omit to leave an existing description unchanged. |
</CommandTable>
@@ -80,5 +80,6 @@ sim secrets set <name> [options]
| --- | --- | --- |
| `--scope <scope>` | Yes | Secret ownership scope. Accepted values: `workspace`, `personal`. |
| `--value <value>` | No | Secret value; visible to shell history when supplied directly. |
| `--description <description>` | No | What the secret is for, shown to teammates; workspace scope only. Omit to leave an existing description unchanged. |
</CommandTable>
@@ -95,14 +95,15 @@ Click **Details** on any secret row to open its detail view.
<Image
src="/static/secrets/secret-details.png"
alt="Secret details view showing Display Name, Description, and Members sections"
alt="Secret details view showing Key, Value, Description, and Members sections"
width={700}
height={400}
/>
From here you can:
- Edit the **Display Name** and **Description**
- View the **Key** and edit the **Value**
- Edit the **Description** — an optional note telling teammates what the secret is for. Workspace secrets only; a personal secret is not shared, so it has none
- Manage **Members** — invite teammates by email and assign them an **Admin** or **Member** role
Click **Save** to apply changes, or **Back** to return to the list.
+26 -1
View File
@@ -5052,6 +5052,17 @@
"enum": ["workspace", "personal"],
"description": "Whether the secret belongs to the workspace or to the caller. A personal secret belongs to the caller across every workspace, not to one workspace."
},
"description": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "What the secret is for, as set on the workspace secret. Always null for a personal secret, which has no shared audience."
},
"role": {
"type": "string",
"enum": ["admin", "member"],
@@ -5070,7 +5081,7 @@
"description": "ISO 8601 timestamp when the secret was last updated."
}
},
"required": ["name", "scope", "role", "createdAt", "updatedAt"],
"required": ["name", "scope", "description", "role", "createdAt", "updatedAt"],
"additionalProperties": false,
"title": "Secret metadata",
"description": "Public secret metadata without the stored secret value."
@@ -5107,6 +5118,7 @@
{
"name": "STRIPE_API_KEY",
"scope": "workspace",
"description": "Production billing key — rotate quarterly.",
"role": "admin",
"createdAt": "2026-06-01T09:14:00.000Z",
"updatedAt": "2026-06-20T14:02:11.000Z"
@@ -5133,6 +5145,7 @@
"data": {
"name": "STRIPE_API_KEY",
"scope": "workspace",
"description": "Production billing key — rotate quarterly.",
"role": "admin",
"createdAt": "2026-06-01T09:14:00.000Z",
"updatedAt": "2026-06-20T14:02:11.000Z"
@@ -5160,6 +5173,18 @@
"maxLength": 65536,
"description": "Write-only secret value. It is never returned.",
"writeOnly": true
},
"description": {
"description": "What the secret is for, shown to teammates. Workspace scope only — sending it for a personal secret is rejected. Omit it to leave an existing description untouched; send null or an empty string to clear one.",
"anyOf": [
{
"type": "string",
"maxLength": 500
},
{
"type": "null"
}
]
}
},
"required": ["workspaceId", "scope", "value"],