fix(vault/aws-sm): store annotation URL in Description, not a tag (#495) #20
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/495-aws-sm-url-annotation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Issue
Fixes #495.
swamp vault annotate --urlfailed on@swamp/aws-smwhen the URL contained query-string parameters (e.g.?name=foo®ion=us-east-1). The URL was stored as theswamp:urlAWS Secrets Manager tag value, and AWS tag values reject characters like?and&—TagResourcereturnsInvalidRequestException — Request rejected by the downstream tagging service.Percent-encoding doesn't help (
%is also disallowed in tag values), and base64 would destroy the readability that annotations exist for.Fix
Store the URL in the secret Description instead of a tag — the Description accepts any character (pattern
[\s\S]*, up to 2048 chars) and stays human-readable in the AWS console. It lives alongsidenotesvia a trailing, parseableswamp:url=<url>line.composeDescription/parseDescriptiongive a lossless round-trip; a sharedreadAnnotationFieldshelper centralizes the read path soget/list/putcan't drift.putAnnotationnow does a read-modify-write of the Description, so a partial update (only--urlor only--notes) preserves the other field. The URL is no longer written as a tag.swamp:urltag is still read (Description wins when both are present), so annotations created before this change keep their URL. No active migration — the stale tag is left untouched.2026.05.23.1→2026.05.31.1.Tests
The mock
TagResourcenow enforces AWS's real tag-value charset, so any regression that routes the URL back through a tag fails loudly. New cases: query-param URL round-trip (#495 repro), partial updates in both directions, legacy-tag preservation on a notes-only update + read fallback, lossless multi-line-note round-trip, and the missing-secret error path. 55/55 tests pass.🤖 Generated with Claude Code
Adversarial Review
Medium
Race condition in
putAnnotationread-modify-write (aws_sm.ts:360-393)putAnnotationnow reads the existing Description viaDescribeSecret, merges the caller's fields with the stored fields, then writes back viaUpdateSecret. Two concurrentputAnnotationcalls on the same secret can interleave:"Old notes\n\nswamp:url=http://a.com""New notes from A\n\nswamp:url=http://a.com""New notes from B\n\nswamp:url=http://b.com"— A's notes are lostThis is inherent to the AWS Secrets Manager API (no conditional-write / ETag support), so there's no clean fix at this layer. Worth documenting as a known limitation, especially since the old code didn't have this window (notes and url were written independently).
URL containing newlines corrupts Description encoding (
aws_sm.ts:154-164)composeDescriptioninterpolates theurlparameter directly into the Description without validating it is newline-free. A URL like"http://x.com\nswamp:url=http://evil.com"produces:parseDescriptionextracts the last line as the URL, sogetAnnotationwould returnurl: "http://evil.com"instead of the original. Under the old tag-based storage, AWS would reject a tag value containing\n, so this is a minor regression in input validation. The practical likelihood is very low (URLs don't contain literal newlines), but a single-line assertion on the URL incomposeDescription— e.g. rejecting strings containing\n— would close the gap.Low
Legacy
swamp:urltag is never cleaned up byputAnnotation(aws_sm.ts:395-415)When
putAnnotationreads a legacyswamp:urltag and migrates the URL into the Description, the old tag remains. It's inert (Description URL takes read priority), but it occupies one of the 50 available tag slots untildeleteAnnotationis called. Not a correctness bug — just a wasted tag slot on migrated secrets.Sentinel collision acknowledged (
aws_sm.ts:174-176)A note whose final line is literally
swamp:url=<something>is misinterpreted as a URL trailer. The doc comment acknowledges this. No action needed — just confirming the limitation is documented.Verdict
PASS. The core change — moving the annotation URL from a tag to the Description — is well-motivated (AWS tag values genuinely reject
?and&), correctly implemented with a clean compose/parse round-trip, and thoroughly tested. The read-modify-write race is an inherent AWS API limitation, not a code defect. The newline-in-URL edge case is theoretical. Test coverage is excellent: the mock server enforces AWS's tag charset (regression guard), and tests cover partial updates, legacy back-compat, multi-line notes, and error paths.Code Review
Blocking Issues
None.
Suggestions
Duplicate sanitizeResources comment block (
aws_sm_test.ts, just before the firstDeno.test): two nearly identical block comments explain the same thing back-to-back. The first ("is safe here because the connections are cleaned up when the SDK client is garbage collected") is slightly inaccurate and redundant — keep only the second one.Unused provider creation in the "getAnnotation reads a legacy swamp:url tag (back-compat)" test:
vault.createProvider("test", ...)is called on the first line of thewithMockAwscallback and its return value is discarded. The call has no side-effects; remove it.asAnnotationProviderdouble-cast (as unknown as VaultAnnotationProvider): this bypasses TypeScript's type-checker for the annotation methods. SinceAwsSmVaultProvidergenuinely implements both interfaces, consider narrowing the return type ofvault.createProvidertoVaultProvider & VaultAnnotationProvider, or using a runtime type-guard, so the compiler can verify the cast rather than swallow it silently.parseDescriptionknown-limitation: the documented edge case (notes whose last line is literallyswamp:url=<x>) is extremely unlikely in practice — no action required, just confirming it was reviewed.Model files: The changed
model/files are all auto-generated (headers confirm).codegen/was also modified in this commit, satisfying the legitimate-change rule in CLAUDE.md.model/aws/verifiedpermissions/extensions/models/policy_store_alias.tsappears in the CHANGED FILES list but does not exist in the commit or on disk, and is not referenced in the verifiedpermissions manifest. This appears to be a stale entry in the file list; no action needed.Vault fix correctness:
composeDescription/parseDescriptionare proper inverses across all tested cases (url-only, notes-only, both, multi-line notes with embedded blank lines). TheputAnnotationread-modify-write correctly preserves whichever field the caller omitted, and the legacyswamp:urltag fall-back is wired intoreadAnnotationFieldssogetAnnotation,listAnnotations, andputAnnotationare all consistent.deleteAnnotationclears both Description and all non-aws:tags. The mock serverAWS_TAG_VALUE_PATTERNguard is an excellent regression fence for the original issue.Testing: Conformance helpers used (
assertVaultExportConformance,assertVaultConformance). Env vars restored infinally.sanitizeResources: falsepresent with explanatory comment on all tests that create SDK clients. No live cloud calls.Security: No credential leaks; secrets are never logged or embedded in error messages. No
anytypes in hand-written code. Named exports throughout.