Skip to content

fix(doppler): accept any secret name, null values and full config inheritance - #23

Merged
pattobrien merged 1 commit into
mainfrom
fix/doppler-open-secret-records
Oct 14, 2026
Merged

pattobrien merged 1 commit into
mainfrom
fix/doppler-open-secret-records

Conversation

@pattobrien

Copy link
Copy Markdown
Owner

Summary

Doppler's OpenAPI spec models secret maps from its example (STRIPE, ALGOLIA, DATABASE). It also leaves out the config inheritance fields on most config responses and misses several nullable and credential fields. The fix is spec patches only. src/services/doppler.ts is regenerated.

 updateSecret request
-  secrets: { STRIPE?, ALGOLIA?, DATABASE? }
+  secrets: Record<string, string | null>           # null deletes
   change_requests[]
-    originalName: string       (required)
-    value: string
+    originalName?: string | null
+    value: string | Redacted | null                 # null keeps value
+    originalValue?: string | Redacted
 listSecrets / updateSecret response
-  secrets: { STRIPE?, ALGOLIA?, DATABASE?, USER? }
+  secrets: Record<string, { raw, computed: string | Redacted | null, note, rawVisibility, computedVisibility, rawValueType, computedValueType }>
 getSecret.value.raw / computed
-  string
+  string | Redacted | null                          # null when restricted
 every config response (list, create, get, rename, clone, lock, unlock)
+  slug, inheritable, inheriting, inherits[{project, config}], inheritedBy[{project, config}]
 service account identity config.claims
-  { aud: string[], sub: string[] }
+  Record<string, string[]>                          # custom and JSON Pointer claims
 serviceTokensCreate.token.key, serviceTokensDelete.token, authOidc request token
+  Redacted
 last_seen_at (me, identities, service account tokens), expires_at (service tokens, service account tokens)
+  | null
Patch Change
secrets.patch.json (new, absorbs secret-visibility.patch.json) open secret records, nullable change request fields, sensitive raw/computed/value/originalValue
config-object.patch.json (new) slug and inheritance fields on every config copy, {project, config} items
service-account-identities.patch.json (new) open claims map, moves ttl_seconds/created_at/last_seen_at from create's config onto the identity, nullable last_seen_at
service-tokens.patch.json (new) sensitive key and delete token, typed nullable expires_at (was unknown)
service-account-tokens.patch.json (new) nullable last_seen_at (create was unknown) and expires_at
auth-me.patch.json (new) nullable last_seen_at
oidc-auth.patch.json sensitive request token

Sources are the Doppler CLI (pkg/models/api.go:156-170, pkg/models/parse.go:107-155,308-309,326, pkg/cmd/secrets.go:438-443), the Terraform provider (doppler/resource_secret.go:136-149,197-198, doppler/models.go:247-266,362-379), the docs, and live calls. Each patch's description cites its own.

Evidence

Repro script (not committed) through the generated SDK with ResponseValidation.strict and mocked responses. Lines that changed:

  • Before:

    S.encode UpdateSecretRequestSecrets: {"STRIPE":"y"}
    S.encode change request (originalName null, value null): FAILED Expected string |   at ["originalName"]
    getSecret restricted: FAILED DopplerParseError: SchemaError(Expected string | undefined |   at ["value"]["raw"])
    getSecret masked: {"name":"MY_KEY","value":{"raw":"x","computed":"x",...
    serviceTokensCreate: {"token":{"name":"ci","slug":"s","key":"dp.st.dev.SECRETKEY","expires_at":null,"access":"read"},"success":true}
    authMe: FAILED DopplerParseError: SchemaError(Expected string | undefined |   at ["last_seen_at"])
    

    After:

    S.encode UpdateSecretRequest secrets: {"MY_KEY":"x","STRIPE":"y"}
    S.encode change request (originalName null, value null): {"name":"NEW_KEY","originalName":null,"value":null}
    getSecret restricted: {"name":"LOCKED","value":{"raw":null,"computed":null,"note":"","rawVisibility":"restricted",...
    getSecret masked: {"name":"MY_KEY","value":{"raw":"<redacted>","computed":"<redacted>",...
    serviceTokensCreate: {"token":{"name":"ci","slug":"s","key":"<redacted>","expires_at":null,"access":"read"},"success":true}
    createIdentity: {"response":{"identity":{...,"claims":{"aud":["doppler"],"sub":["repo:o/r:ref:refs/heads/main"],"environment":["prod"]}},"ttl_seconds":3600,"created_at":"2026-10-13T00:00:00.000Z","last_seen_at":null},"success":true},...}
    authMe: {"slug":"s",...,"last_seen_at":null,...,"success":true}
    
  • Before (caller code type-checked against the SDK):

    error TS2353: Object literal may only specify known properties, and 'MY_KEY' does not exist in type 'UpdateSecretRequestSecrets'.
    error TS2322: Type 'null' is not assignable to type 'string'.
    error TS2322: Type 'null' is not assignable to type 'string'.
    error TS7053: Element implicitly has an 'any' type because expression of type '"MY_KEY"' can't be used to index type 'ListSecretsResponseSecrets'.
    error TS2339: Property 'inherits' does not exist on type 'CloneConfigResponseConfig'.
    error TS2353: Object literal may only specify known properties, and 'environment' does not exist in type 'PostV3workplaceserviceAccountsserviceAccountServiceAccountIdentitiesRequestConfigClaims'.
    

    After: tsc exit=0

  • Live (2026-10-13, config service token, read-only, strict validation, shapes only):

    authMe OK {..."last_seen_at":"string"...}            # was null on the first call of the day
    getConfig OK {"config":{...,"slug":"string","inheritable":"boolean","inheriting":"boolean","inherits":[],"inheritedBy":[]},...}
    listConfigs OK {"page":"number","configs":[{...,"slug":"string","inheritable":"boolean","inheriting":"boolean","inherits":[]}],...}
    listEnvironments OK {"environments":[],"page":"number","success":"boolean"}
    secretsNames OK {"names":["string"],"success":"boolean"}
    getSecret OK {"name":"string","value":{"raw":"Redacted","computed":"Redacted",...}}
    listSecrets OK {"secretCount":"number","distinctPerSecretShapes":[{"raw":"string","computed":"string",...}]}
    

    There was no full create/delete e2e. 1Password holds only config-scoped service tokens, which cannot create projects, environments, service accounts or service tokens.

  • pnpm typecheck:ci, pnpm specs:check, pnpm lint and pnpm patches:audit doppler all pass. The audit reports 9 patch files, all needed.

Not fixed here:

  • listSecrets/updateSecret record values and the secrets request map are typed Redacted but stay plain strings at runtime (live listSecrets above). Core wrapSensitive skips Records, and the generator drops sensitive on map values. That needs a separate core PR.
  • Config initial_fetch_at/last_fetch_at may be null for never-fetched configs. There is no evidence yet.
  • Environment create project is a query param in the spec, but both clients send it in the body. Service token expire_at is a string in the spec, but the CLI sends an integer. Neither is verified live.

Merge Danger

Door: two-way

Revert the commit to roll back. Nothing is persisted or migrated.

Blast Radius: type-names

Exported types renamed or removed: UpdateSecretRequestSecrets becomes UpdateSecretRequestSecretsMap. ListSecretsResponseSecrets* and UpdateSecretResponseSecrets* become ...SecretsMap/...SecretsValue. The identity ...ConfigClaims, ...ClaimsAudList and ...ClaimsSubList types become ...ClaimsMap and ...ClaimsValueList. The other config response types are no longer aliases of CloneConfigResponseConfig. Callers that imported those names will fail to compile. Callers of the operations keep working, and fields that changed to Redacted now need Redacted.value.

https://claude.ai/code/session_01EYBKWh5PaTXsDr9XVLm7sD

…eritance

Callers can now pass any secret name to updateSecret and read any secret
name from listSecrets and updateSecret responses. The spec had modeled
these maps from its STRIPE/ALGOLIA/DATABASE example. A change request
accepts originalName and value as null, as the docs and both Doppler
clients require. Restricted secrets decode with null raw/computed.

Every config response now carries slug, inheritable, inheriting,
inherits and inheritedBy. Service account identity claims accept any
claim name. Service token keys and OIDC request tokens are Redacted.
Never-seen last_seen_at and no-expiry expires_at decode as null.

secret-visibility.patch.json is folded into secrets.patch.json.

Claude-Session: https://claude.ai/code/session_01EYBKWh5PaTXsDr9XVLm7sD
@pattobrien
pattobrien merged commit b938d4b into main Oct 14, 2026
2 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant