fix: support secret: prefixed UUID refs in ctx.secrets.resolve()#1599
fix: support secret: prefixed UUID refs in ctx.secrets.resolve()#1599r3ptar wants to merge 2 commits intopaperclipai:masterfrom
Conversation
Plugins that store secrets programmatically via the platform REST API receive UUID references back (e.g. "secret:a1b2c3d4-..."). The resolve handler rejected these because it only accepted bare UUIDs and because the config-only scope check blocked any ref not present in the plugin's instance config. - Add parseSecretRef() to strip the "secret:" prefix and validate the UUID, with input length limits and empty-string rejection - Remove the config-based scope check that blocked runtime-created secrets (the UUID itself is an unguessable 122-bit capability token, protected by the existing 30/min rate limiter) - Clean up dead imports (pluginConfig, pluginCompanySettings, desc, pluginRegistryService) - Add 19 unit tests for parseSecretRef and extractSecretRefsFromConfig Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR extends the plugin credential-resolution handler to accept prefixed UUID refs (e.g.
Unit-test coverage for Confidence Score: 2/5
Important Files Changed
|
- Add company boundary check to secret lookup: query plugin_company_settings for the plugin's associated companies and filter the companySecrets query with inArray(companyId). When a plugin has no company settings rows (enabled for all companies by default), the filter is skipped with an explicit comment explaining the fallback. - Fix UUID regex comment: "UUID v4" -> "RFC-4122 UUID (any version)" since the pattern does not constrain the version nibble. - Document rate limiter as best-effort/process-local: clarify that in multi-instance deployments the effective limit is 30 x N, and that defence-in-depth relies on UUID entropy + company scoping. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Thinking Path
Paperclip orchestrates AI agents for zero-human companies
Plugins extend Paperclip with integrations like Microsoft 365, and these plugins often need to store sensitive credentials like OAuth tokens and API keys
The platform provides a secure secrets API so plugins don't have to store raw values in plaintext state
When a plugin stores a secret through the platform, it gets back a UUID reference (sometimes prefixed as secret:)
But ctx.secrets.resolve() only accepted bare UUIDs, and a config-only scope check rejected any UUID not already present in the plugin's instance config
This meant plugins that created secrets at runtime could never resolve them, forcing authors to store raw secret values in ctx.state as plaintext in the database — a security risk
This PR fixes ctx.secrets.resolve() to accept both bare and secret:-prefixed UUIDs, and removes the overly restrictive scope check so runtime-created secrets can be resolved
The benefit is that plugin authors can now use the platform's secret storage as intended, keeping credentials encrypted rather than stored as plaintext in plugin state
What I did
Added parseSecretRef() which strips the secret: prefix, validates UUID format, enforces a 256-char length limit, and rejects empty strings. Removed the config-based scope check that was blocking runtime-created secrets — the UUID itself is a 122-bit unguessable capability token, and the existing 30/min rate limiter prevents enumeration. Cleaned up the dead imports and unused cache code that resulted from the scope check removal. Updated JSDoc throughout to document the new format.
Why it matters
Plugin authors were forced to store raw secret values in ctx.state (plaintext in the database) because ctx.secrets.resolve() couldn't handle the UUID refs the platform returned. This fix closes that gap so the platform's encrypted secret storage works end-to-end.
How to verify
Risks
The scope check removal means any plugin with secrets.read-ref capability can resolve any secret by UUID, not just ones in its config. This is mitigated by UUID entropy making guessing infeasible, the 30/min rate limiter, and capability gating. The previous scope check provided defense-in-depth but blocked the primary use case.