feat(credential-groups): collect API keys from invited people - #7348
feat(credential-groups): collect API keys from invited people#7348TheodoreSpeaks wants to merge 1 commit into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThe PR extends Credential Groups to collect, encrypt, authorize, and resolve invited users' API-key credentials in addition to OAuth credentials.
Confidence Score: 4/5The PR appears safe to merge after correcting the non-blocking Get API Key documentation so workflow authors receive valid provider and field examples. Authorization, persistence, migration, and redaction paths preserve the relevant boundaries, but the new block guidance points users to an unsupported provider and nonexistent output keys. Files Needing Attention: apps/sim/blocks/blocks/credential-group.ts
|
| Filename | Overview |
|---|---|
| apps/sim/lib/credential-groups/api-key.ts | Transactionally persists verified API-key credentials with enrollment and option locking. |
| apps/sim/lib/credentials/managed-api-key-resolution.ts | Resolves active managed API keys after workspace, entitlement, type, and provider validation. |
| apps/sim/executor/handlers/credential-group/credential-group-handler.ts | Adds authorized get_api_key execution and registers each secret field for trace redaction before returning it. |
| apps/sim/app/api/credential-groups/enroll/[token]/api-key/[optionId]/route.ts | Adds the rate-limited public route for validating and submitting invited users' API-key fields. |
| packages/db/migrations/0313_credential_managed_api_key_storage.sql | Adds encrypted managed-key storage, consistency constraints, and widened option uniqueness. |
| apps/sim/blocks/blocks/credential-group.ts | Adds the get_api_key block surface, but its help text names unsupported Gong fields instead of the supported AWS field contract. |
Sequence Diagram
sequenceDiagram
participant Invitee
participant Enrollment as Enrollment API
participant Provider
participant DB
participant Workflow
participant Policy
participant Registry as Redaction Registry
Invitee->>Enrollment: Submit provider fields
Enrollment->>Provider: Verify credentials
Provider-->>Enrollment: Verified identity
Enrollment->>DB: Encrypt and persist managed_api_key
Workflow->>Policy: Authorize group and credential access
Policy-->>Workflow: Allowed
Workflow->>DB: Resolve encrypted credential
Workflow->>Registry: Register secret provenance
Registry-->>Workflow: Accepted
Workflow-->>Workflow: Return fields to block
Reviews (1): Last reviewed commit: "feat(credential-groups): collect API key..." | Re-trigger Greptile
| - "List Credentials" returns active, usable credentials only. Reconnect-needed and revoked credentials are excluded. | ||
| - Use "List People" to inspect invitation and connection progress without exposing credential secrets. | ||
| - "Send Invite" sends one email. Use a loop when invitations should come from a dynamic list. | ||
| - "Get API Key" returns one enrolled person's credential values under "fields" — reference them as <block.fields.apiKey>, or <block.fields.accessKey> and <block.fields.accessKeySecret> for Gong. Pass the credentialId from "List Credentials". |
There was a problem hiding this comment.
Documentation names unsupported key fields
The new operation guidance names Gong, which is not registered as an API-key provider, and recommends accessKey and accessKeySecret, while the supported AWS provider consumes accessKeyId and secretAccessKey. Workflow authors following these examples will reference undefined output fields.
Credential Groups could only collect managed OAuth grants, so services whose per-person credential is an API key — meeting recorders, AWS — had no way in. An option can now ask each invited person for a key instead of a sign-in. Storage is one new credential type and one new column. `credential.encryptedApiKey` holds a versioned envelope of the provider's declared fields, so a service needing more than one value (AWS: access key id, secret, region) needs no separate shape. It is sealed with `encryptSecret`, never `encryptApiKey`: the resolved-secret trace registry decrypts with `decryptSecret`, and `encryptApiKey` silently stores plaintext when its key is unset. Consumption is a new `get_api_key` block operation rather than a credential-id socket, so every existing API-key block works unchanged. It authorizes through the same resource policy as managed OAuth — actors may use their own credential, other enrollments need a workflow access grant — then registers one catalog entry per secret field with the run's trace registry before returning. A run without a registry fails the block rather than emitting a secret nothing can redact. Fields declared `secret: false` (a region) are stored but never catalogued: substituting a short recurring string would corrupt unrelated log output. Secret fields are held to the 8-character floor below which the matcher deliberately never redacts. Providers are AWS, Fireflies, Grain and Granola — each one a credential an ordinary user can create for themselves. Gong was excluded on that test: its access keys are workspace-scoped and admin-only, so they are the company's credential, not a person's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcW9yNMBT7PkaB2LYJgEaM
3e9204a to
04d7873
Compare
There was a problem hiding this comment.
14 issues found across 45 files
Confidence score: 2/5
apps/sim/lib/credentials/managed-api-key.tscan return a stored secret without a provenance entry, allowing theget_api_keyblock to expose data that has not been validated. Ensure every returned secret has matching provenance or reject the envelope.apps/sim/lib/credential-groups/service.tsleaves existingmanaged_api_keycredentials active after an API-key option is removed or its group is disabled, so revoked access may continue working. Includemanaged_api_keyrows in the invalidation update.apps/sim/lib/credentials/access.tsadmits newly enrolledmanaged_api_keycredentials beforerequireOrdinaryCredentialTypethrows, breaking list, detail, lookup, and membership endpoints. Update the query/type handling so these endpoints consistently exclude or support managed credentials.apps/sim/lib/credential-groups/api-key-providers/grain.tsandaws.tscan reject valid Grain or GovCloud/China enrollments because verification uses incorrect request details. Match Grain’s required method and API-version header, and derive AWS’s STS host and signing region from the selected region.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/sim/blocks/blocks/credential-group.ts">
<violation number="1" location="apps/sim/blocks/blocks/credential-group.ts:119">
P2: The documented field names do not match any supported provider, so workflows following this guidance will reference undefined fields. Update both this guidance and the `fields` description to document AWS as `accessKeyId`/`secretAccessKey` and the other providers as `apiKey`; Gong is not supported.</violation>
</file>
<file name="apps/sim/lib/credentials/managed-api-key.ts">
<violation number="1" location="apps/sim/lib/credentials/managed-api-key.ts:151">
P1: When a stored envelope omits a declared secret or contains an undeclared secret field, this loop returns the value without a provenance entry. The `get_api_key` block then marks provenance complete and can expose that credential in logs or model-visible output; validate the decrypted fields against `declaredFields` before building provenance.</violation>
</file>
<file name="apps/sim/lib/credential-groups/api-key-providers/grain.ts">
<violation number="1" location="apps/sim/lib/credential-groups/api-key-providers/grain.ts:21">
P1: When a valid Grain key is enrolled, this probe uses the wrong method and omits Grain's required API-version header, so Grain rejects the verification before enrollment succeeds. Match the existing Grain `/teams` request contract by using `POST` and sending `Content-Type` plus `Public-Api-Version: '2025-10-31'`.</violation>
</file>
<file name="apps/sim/lib/credential-groups/service.ts">
<violation number="1" location="apps/sim/lib/credential-groups/service.ts:292">
P1: When an API-key option is removed or its group is disabled, existing `managed_api_key` credentials remain active because the invalidation update targets only OAuth rows. Include `managed_api_key` in that status update so existing workflow references stop resolving the key.</violation>
</file>
<file name="apps/sim/lib/credential-groups/api-key-providers/aws.ts">
<violation number="1" location="apps/sim/lib/credential-groups/api-key-providers/aws.ts:19">
P2: When an invitee supplies a valid GovCloud or China region, this verifier still calls commercial `sts.amazonaws.com` and signs `us-east-1`, so AWS rejects the credential. Select the STS host and signing region from `fields.region`, or reject unsupported partitions before verification.</violation>
<violation number="2" location="apps/sim/lib/credential-groups/api-key-providers/aws.ts:86">
P2: After receiving STS's response, this path neither consumes nor cancels its body, leaving the undrained response to tie up the server's HTTP connection until cleanup. Cancel the body before status branching when only the status is needed.</violation>
<violation number="3" location="apps/sim/lib/credential-groups/api-key-providers/aws.ts:86">
P2: When STS stalls after the connection attempt, `verify` has no deadline and the enrollment request waits on the runtime's fetch timeout. Pass a bounded `AbortSignal.timeout(...)` to this request and map timeout to the existing retryable error.</violation>
</file>
<file name="apps/sim/lib/credentials/access.ts">
<violation number="1" location="apps/sim/lib/credentials/access.ts:37">
P1: When a newly enrolled `managed_api_key` reaches an ordinary credential query, the query admits it but `requireOrdinaryCredentialType` now throws. The list, detail, lookup, and membership endpoints therefore fail instead of omitting managed credentials; update all ordinary filters to exclude `MANAGED_CREDENTIAL_TYPES`.</violation>
</file>
<file name="apps/sim/lib/credential-groups/api-key.ts">
<violation number="1" location="apps/sim/lib/credential-groups/api-key.ts:69">
P2: When email delivery fails after this request resolves its context, the enrollment becomes `delivery_failed`, but this check still accepts it and persists the API key. Reject `delivery_failed` here so failed invitations cannot be enrolled during that race.</violation>
</file>
<file name="packages/db/migrations/meta/_journal.json">
<violation number="1" location="packages/db/migrations/meta/_journal.json:2204">
P2: These journal entries advance the migration history without matching schema snapshots for the enum, column, constraint, and index changes. Add the generated `0315`/`0316` migration snapshots so the next schema migration starts from the deployed managed-API-key state instead of diffing from `0314`.\n\n(Based on your team's feedback about deployable, synchronized migration artifacts.)</violation>
</file>
<file name="apps/sim/lib/credential-groups/api-key-providers/fireflies.ts">
<violation number="1" location="apps/sim/lib/credential-groups/api-key-providers/fireflies.ts:24">
P2: If Fireflies accepts the connection but stalls, this enrollment request can remain occupied for the transport default instead of failing promptly. Add a finite `AbortSignal.timeout(...)` to the provider request and let the existing transport-error handler return the retryable verification message.</violation>
<violation number="2" location="apps/sim/lib/credential-groups/api-key-providers/fireflies.ts:35">
P2: When Fireflies returns a rejected or otherwise non-2xx response, cancel the response body before throwing so invalid-key attempts do not retain the upstream connection. Apply the cancellation to both status branches.</violation>
<violation number="3" location="apps/sim/lib/credential-groups/api-key-providers/fireflies.ts:50">
P2: When Fireflies returns a malformed successful payload with a non-string `email` or `name`, this verifier throws a raw `TypeError` and the enrollment endpoint returns 500. Validate the response field types before normalizing them and convert malformed payloads to `CredentialGroupApiKeyVerificationError`.</violation>
</file>
<file name="packages/db/migrations/0316_credential_managed_api_key_storage.sql">
<violation number="1" location="packages/db/migrations/0316_credential_managed_api_key_storage.sql:21">
P2: When a concurrent index build is cancelled or fails, the migration runner retries this file from the top, but this constraint creation is not idempotent. Wrap the constraint creation in a duplicate-object guard so the replay can reach the index rebuild.</violation>
</file>
Heads up: you’re close to your included review allowance. Set a flex budget so reviews don’t pause.
Re-trigger cubic
| throw new ManagedApiKeyFormatError('Invalid managed API key envelope') | ||
| } | ||
|
|
||
| const secretFieldIds = new Set( |
There was a problem hiding this comment.
P1: When a stored envelope omits a declared secret or contains an undeclared secret field, this loop returns the value without a provenance entry. The get_api_key block then marks provenance complete and can expose that credential in logs or model-visible output; validate the decrypted fields against declaredFields before building provenance.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/credentials/managed-api-key.ts, line 151:
<comment>When a stored envelope omits a declared secret or contains an undeclared secret field, this loop returns the value without a provenance entry. The `get_api_key` block then marks provenance complete and can expose that credential in logs or model-visible output; validate the decrypted fields against `declaredFields` before building provenance.</comment>
<file context>
@@ -0,0 +1,162 @@
+ throw new ManagedApiKeyFormatError('Invalid managed API key envelope')
+ }
+
+ const secretFieldIds = new Set(
+ declaredFields.filter((field) => field.secret).map((field) => field.id)
+ )
</file context>
| let response: Response | ||
| try { | ||
| response = await fetch(GRAIN_TEAMS_URL, { | ||
| method: 'GET', |
There was a problem hiding this comment.
P1: When a valid Grain key is enrolled, this probe uses the wrong method and omits Grain's required API-version header, so Grain rejects the verification before enrollment succeeds. Match the existing Grain /teams request contract by using POST and sending Content-Type plus Public-Api-Version: '2025-10-31'.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/credential-groups/api-key-providers/grain.ts, line 21:
<comment>When a valid Grain key is enrolled, this probe uses the wrong method and omits Grain's required API-version header, so Grain rejects the verification before enrollment succeeds. Match the existing Grain `/teams` request contract by using `POST` and sending `Content-Type` plus `Public-Api-Version: '2025-10-31'`.</comment>
<file context>
@@ -0,0 +1,45 @@
+ let response: Response
+ try {
+ response = await fetch(GRAIN_TEAMS_URL, {
+ method: 'GET',
+ headers: { Authorization: `Bearer ${apiKey}` },
+ })
</file context>
| const invalidatedOptionIds = existing.options | ||
| .filter((option) => { | ||
| const next = nextOptionById.get(option.id) | ||
| if (!next || body.status === 'disabled') return true |
There was a problem hiding this comment.
P1: When an API-key option is removed or its group is disabled, existing managed_api_key credentials remain active because the invalidation update targets only OAuth rows. Include managed_api_key in that status update so existing workflow references stop resolving the key.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/credential-groups/service.ts, line 292:
<comment>When an API-key option is removed or its group is disabled, existing `managed_api_key` credentials remain active because the invalidation update targets only OAuth rows. Include `managed_api_key` in that status update so existing workflow references stop resolving the key.</comment>
<file context>
@@ -262,12 +289,15 @@ export async function updateCredentialGroup(
const invalidatedOptionIds = existing.options
.filter((option) => {
const next = nextOptionById.get(option.id)
+ if (!next || body.status === 'disabled') return true
+ // An API-key option carries no scope policy, so nothing about editing it can
+ // invalidate a key its owner already pasted. Only removal or disabling does.
</file context>
| export function requireOrdinaryCredentialType(type: CredentialType): OrdinaryCredentialType { | ||
| if (type === 'managed_oauth') { | ||
| throw new Error('Managed OAuth credential reached an ordinary credential surface') | ||
| if (isManagedCredentialType(type)) { |
There was a problem hiding this comment.
P1: When a newly enrolled managed_api_key reaches an ordinary credential query, the query admits it but requireOrdinaryCredentialType now throws. The list, detail, lookup, and membership endpoints therefore fail instead of omitting managed credentials; update all ordinary filters to exclude MANAGED_CREDENTIAL_TYPES.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/credentials/access.ts, line 37:
<comment>When a newly enrolled `managed_api_key` reaches an ordinary credential query, the query admits it but `requireOrdinaryCredentialType` now throws. The list, detail, lookup, and membership endpoints therefore fail instead of omitting managed credentials; update all ordinary filters to exclude `MANAGED_CREDENTIAL_TYPES`.</comment>
<file context>
@@ -12,12 +12,30 @@ type ActiveCredentialMember = typeof credentialMember.$inferSelect
export function requireOrdinaryCredentialType(type: CredentialType): OrdinaryCredentialType {
- if (type === 'managed_oauth') {
- throw new Error('Managed OAuth credential reached an ordinary credential surface')
+ if (isManagedCredentialType(type)) {
+ throw new Error('Managed credential reached an ordinary credential surface')
}
</file context>
| - "List Credentials" returns active, usable credentials only. Reconnect-needed and revoked credentials are excluded. | ||
| - Use "List People" to inspect invitation and connection progress without exposing credential secrets. | ||
| - "Send Invite" sends one email. Use a loop when invitations should come from a dynamic list. | ||
| - "Get API Key" returns one enrolled person's credential values under "fields" — reference them as <block.fields.apiKey>, or <block.fields.accessKey> and <block.fields.accessKeySecret> for Gong. Pass the credentialId from "List Credentials". |
There was a problem hiding this comment.
P2: The documented field names do not match any supported provider, so workflows following this guidance will reference undefined fields. Update both this guidance and the fields description to document AWS as accessKeyId/secretAccessKey and the other providers as apiKey; Gong is not supported.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/blocks/blocks/credential-group.ts, line 119:
<comment>The documented field names do not match any supported provider, so workflows following this guidance will reference undefined fields. Update both this guidance and the `fields` description to document AWS as `accessKeyId`/`secretAccessKey` and the other providers as `apiKey`; Gong is not supported.</comment>
<file context>
@@ -109,6 +116,7 @@ export const CredentialGroupBlock: BlockConfig<CredentialGroupBlockOutput> = {
- "List Credentials" returns active, usable credentials only. Reconnect-needed and revoked credentials are excluded.
- Use "List People" to inspect invitation and connection progress without exposing credential secrets.
- "Send Invite" sends one email. Use a loop when invitations should come from a dynamic list.
+ - "Get API Key" returns one enrolled person's credential values under "fields" — reference them as <block.fields.apiKey>, or <block.fields.accessKey> and <block.fields.accessKeySecret> for Gong. Pass the credentialId from "List Credentials".
- "Get Invite Link" issues a fresh seven-day bearer link without sending email. It invalidates the previous link for that email, so treat the output as a secret.
`,
</file context>
| "breakpoints": true | ||
| }, | ||
| { | ||
| "idx": 315, |
There was a problem hiding this comment.
P2: These journal entries advance the migration history without matching schema snapshots for the enum, column, constraint, and index changes. Add the generated 0315/0316 migration snapshots so the next schema migration starts from the deployed managed-API-key state instead of diffing from 0314.\n\n
(Based on your team's feedback about deployable, synchronized migration artifacts.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/migrations/meta/_journal.json, line 2204:
<comment>These journal entries advance the migration history without matching schema snapshots for the enum, column, constraint, and index changes. Add the generated `0315`/`0316` migration snapshots so the next schema migration starts from the deployed managed-API-key state instead of diffing from `0314`.\n\n
(Based on your team's feedback about deployable, synchronized migration artifacts.) </comment>
<file context>
@@ -2199,6 +2199,20 @@
"breakpoints": true
+ },
+ {
+ "idx": 315,
+ "version": "7",
+ "when": 1788208210301,
</file context>
| ) | ||
| } | ||
|
|
||
| if (response.status === 401 || response.status === 403) { |
There was a problem hiding this comment.
P2: When Fireflies returns a rejected or otherwise non-2xx response, cancel the response body before throwing so invalid-key attempts do not retain the upstream connection. Apply the cancellation to both status branches.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/credential-groups/api-key-providers/fireflies.ts, line 35:
<comment>When Fireflies returns a rejected or otherwise non-2xx response, cancel the response body before throwing so invalid-key attempts do not retain the upstream connection. Apply the cancellation to both status branches.</comment>
<file context>
@@ -0,0 +1,64 @@
+ )
+ }
+
+ if (response.status === 401 || response.status === 403) {
+ throw new CredentialGroupApiKeyVerificationError('Fireflies rejected this API key.')
+ }
</file context>
| const apiKey = fields.apiKey | ||
| let response: Response | ||
| try { | ||
| response = await fetch(FIREFLIES_GRAPHQL_URL, { |
There was a problem hiding this comment.
P2: If Fireflies accepts the connection but stalls, this enrollment request can remain occupied for the transport default instead of failing promptly. Add a finite AbortSignal.timeout(...) to the provider request and let the existing transport-error handler return the retryable verification message.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/credential-groups/api-key-providers/fireflies.ts, line 24:
<comment>If Fireflies accepts the connection but stalls, this enrollment request can remain occupied for the transport default instead of failing promptly. Add a finite `AbortSignal.timeout(...)` to the provider request and let the existing transport-error handler return the retryable verification message.</comment>
<file context>
@@ -0,0 +1,64 @@
+ const apiKey = fields.apiKey
+ let response: Response
+ try {
+ response = await fetch(FIREFLIES_GRAPHQL_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
</file context>
| response = await fetch(FIREFLIES_GRAPHQL_URL, { | |
| response = await fetch(FIREFLIES_GRAPHQL_URL, { | |
| signal: AbortSignal.timeout(15_000), |
| } | ||
|
|
||
| const user = payload?.data?.user | ||
| const email = user?.email ? normalizeEmail(user.email) : undefined |
There was a problem hiding this comment.
P2: When Fireflies returns a malformed successful payload with a non-string email or name, this verifier throws a raw TypeError and the enrollment endpoint returns 500. Validate the response field types before normalizing them and convert malformed payloads to CredentialGroupApiKeyVerificationError.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/credential-groups/api-key-providers/fireflies.ts, line 50:
<comment>When Fireflies returns a malformed successful payload with a non-string `email` or `name`, this verifier throws a raw `TypeError` and the enrollment endpoint returns 500. Validate the response field types before normalizing them and convert malformed payloads to `CredentialGroupApiKeyVerificationError`.</comment>
<file context>
@@ -0,0 +1,64 @@
+ }
+
+ const user = payload?.data?.user
+ const email = user?.email ? normalizeEmail(user.email) : undefined
+ if (!user?.user_id || !email || !isValidEmailSyntax(email)) {
+ throw new CredentialGroupApiKeyVerificationError(
</file context>
| -- Added NOT VALID so it never takes a validating lock against live writes; no stored row can | ||
| -- violate it (none is `managed_api_key` yet), so the VALIDATE below is a formality that takes | ||
| -- only a SHARE UPDATE EXCLUSIVE lock. | ||
| ALTER TABLE "credential" ADD CONSTRAINT "credential_managed_api_key_source_check" CHECK ((type::text <> 'managed_api_key') OR ( |
There was a problem hiding this comment.
P2: When a concurrent index build is cancelled or fails, the migration runner retries this file from the top, but this constraint creation is not idempotent. Wrap the constraint creation in a duplicate-object guard so the replay can reach the index rebuild.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/migrations/0316_credential_managed_api_key_storage.sql, line 21:
<comment>When a concurrent index build is cancelled or fails, the migration runner retries this file from the top, but this constraint creation is not idempotent. Wrap the constraint creation in a duplicate-object guard so the replay can reach the index rebuild.</comment>
<file context>
@@ -0,0 +1,54 @@
+-- Added NOT VALID so it never takes a validating lock against live writes; no stored row can
+-- violate it (none is `managed_api_key` yet), so the VALIDATE below is a formality that takes
+-- only a SHARE UPDATE EXCLUSIVE lock.
+ALTER TABLE "credential" ADD CONSTRAINT "credential_managed_api_key_source_check" CHECK ((type::text <> 'managed_api_key') OR (
+ encrypted_api_key IS NOT NULL
+ AND credential_group_enrollment_id IS NOT NULL
</file context>
|
Parking this — going with MCP connections first. Branch Two things to pick up if this is revived:
|
Summary
managed_api_keycredential type +credential.encrypted_api_key, holding a versioned envelope of the provider's declared fields, so a service needing several values (AWS: key id, secret, region) needs no separate shapeget_api_keyblock operation, authorized through the same resource policy as managed OAuth, so every existing API-key block works unchangedfeat/all-oauth-cred-group(21 OAuth providers), which this work is built onNotes
encryptSecret, neverencryptApiKey: the resolved-secret trace registry decrypts withdecryptSecret, andencryptApiKeysilently stores plaintext when its key is unsetsecret: false(a region) are stored but never catalogued; substituting a short recurring string would corrupt unrelated log outputType of Change
Testing
Tested manually against the enrollment page.
bun run lint,check:api-validation,check:migrations origin/staging, all 37 audits, and the full test suite pass. AWS SigV4 signing is pinned against AWS's published derivation vector plus a golden signature, both mutation-checked. Not yet verified end to end: a live STS round trip and the redaction check against a running workflow.Checklist