feat(1password): add vault delete support #67
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-790/vault-delete-1password"
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?
Summary
delete(secretKey)method toOnePasswordVaultProviderimplementing theVaultDeleteProviderinterface (duck-typed viaisVaultDeleteProviderin swamp core)op item deleteto remove items from 1Password; rejects fullop://URIs (matchingput's behavior); idempotent on missing items2026.06.24.1and description to mention delete supportswamp vault deleteusageTest plan
deno checkpassesdeno lintpassesdeno fmt --checkpassesdeno install --frozenpasses (lockfile clean)deletemethod exposed on providerwithMockedCommand(pre-existing Deno 2.8 incompatibility with@systeminit/swamp-testingaffects all mocked tests locally — CI should pass on compatible Deno version)Closes #790
Code Review
Blocking Issues
deletesilently swallows vault-not-found errors (onepassword.ts:435-443)The
deletemethod catches errors and checkserror.message.includes("not found")to suppress idempotent-delete cases. However,runOpwraps vault-misconfiguration errors as:This message also contains "not found", so a misconfigured vault name causes
deleteto silently return success instead of propagating the error. Auth errors correctly propagate (their message does not contain "not found"), but vault errors do not — inconsistent and likely to mask configuration mistakes.Fix: Tighten the guard, for example:
Alternatively, check the raw
opstderr for the item-not-found pattern beforerunOpwraps it.Suggestions
Misleading test assertion for bracket-key rejection (
onepassword_test.ts:597)The test "putAnnotation rejects label keys with brackets" asserts on the substring
"must not contain dots"even though the rejected key contains brackets ("bad[key]"). The assertion passes because the error message lists multiple forbidden characters in one sentence, but a reader sees bracket rejection tested via a "dots" assertion. Consider asserting on"must not contain"(shared prefix) or the actual character mentioned ("brackets").delete("item/field")deletes the whole item silently (onepassword.ts:412-443)When
secretKeyis"my-item/field",parseSecretKeysetsparsed.item = "my-item", sodeletedestroys the entire item — all fields, not just the named field. This is tested and intentional, but the behavior is asymmetric withputandgetwhich operate at field granularity. The README does not currently mention this. A one-line note in the README's "Secret key format" section (e.g., "deletealways removes the whole item") would prevent surprises.Adversarial Review
Critical / High
onepassword.ts:435-439 -- delete silently swallows vault-not-found errors
The delete method catches errors and suppresses any whose message includes
"not found". The intent is idempotent delete when the item does not exist.
But runOp (line 753-761) also throws errors containing "not found" for vault
misconfiguration. The error message template is:
Breaking example: A user creates a provider with op_vault "Enginering"
(typo). Calling provider.delete("my-secret") silently returns without error.
The user believes the secret was deleted, but it still exists in the real
vault "Engineering". This is a data-retention bug.
Suggested fix: match more specifically. Item-not-found errors from the op CLI
pass through runOp with the prefix "1Password CLI error:". Vault-not-found
errors use the prefix "1Password vault". Check for the CLI error prefix:
This lets vault-configuration and auth errors propagate while still making
item-not-found idempotent.
Medium
onepassword.ts:412-444 -- delete with item/field key silently deletes the
entire item, discarding all other fields
parseSecretKey("my-item/database/password") returns item="my-item", but
delete only uses parsed.item, so op item delete removes the whole item. A
caller who expects field-level delete (analogous to how put targets a
specific field) would lose all other fields and annotations silently.
The test at line 810-819 confirms this is intentional. Suggestion: throw an
error when the field portion is not the default "password", explaining that
field-level delete is not supported by the op CLI.
Low
onepassword.ts:650-668 -- (pre-existing) getItemJson swallows all errors
The bare catch at line 666-668 treats any error as "item not found". Not
introduced by this PR. Noting for awareness only.
Verdict
FAIL -- The overly-broad "not found" error matching in delete means a
misconfigured vault silently pretends the delete succeeded. Fix before merge.
The delete method's "not found" error suppression was too broad — it also caught vault-misconfiguration errors ("1Password vault 'X' not found") because runOp wraps those with a message containing "not found". Tighten the guard to exclude vault-level errors so only item-not-found is treated as idempotent success. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>Code Review
This PR adds
deletesupport to the 1Password vault provider: adelete()method onOnePasswordVaultProvider, the corresponding mock handler increateOpMock(), six new test cases, and updated documentation.Blocking Issues
None.
Suggestions
Delete silently deletes the whole item when given a field-level key (
"my-item/password","my-item/section/field"):parseSecretKeyextracts only the item name, soop item deletealways targets the entire item regardless of whether a field segment was included. The test"delete with item/field key deletes the whole item"explicitly covers and accepts this behaviour, but it is not mentioned in the README's secret key format section. A user who stores multiple fields under one item (e.g.my-db/passwordandmy-db/username) and then callsswamp vault delete my-1password my-db/passwordwill silently losemy-db/usernametoo. Worth a short note in the README under "Secret key format".Fragile string-matching in the idempotency guard (
onepassword.ts:435-444): The conditionerror.message.includes("not found") && !error.message.startsWith("1Password vault")relies on the exact wording of error messages produced byrunOp. If the 1Password CLI ever emits an error containing "not found" for a reason other than a missing item (e.g. "server not found", "config file not found") and that message doesn't trigger one ofrunOp's earlier, more specific checks (auth, vault), it would be silently swallowed. The existing logic is correct for all currently-known error shapes, but a tighter guard — e.g. checking that the generic prefix"1Password CLI error: "is present — would make the intent clearer and reduce the risk of future regressions.Adversarial Review
Medium
Idempotency guard may not match real op CLI error messages - onepassword.ts:436-440
The delete idempotency guard silently returns when the caught error
contains "not found" but does not start with "1Password vault":
This check runs against runOp processed error messages, which embed
the raw stderr from op. The test mock (onepassword_test.ts:300) returns
item "X" not foundas stderr, which matches the guard. However, theop CLI v2.x typically reports missing items differently, e.g.:
That message does NOT contain the substring "not found", so the guard
fails to match and delete throws instead of being idempotent. The test
passes because the mock was written to match the guard by construction,
not because it faithfully reproduces op CLI output.
Breaking scenario:
swamp vault delete my-vault already-deleted-itemthrows an unexpected error instead of succeeding silently.
Suggested fix: Match on additional patterns the real op CLI uses
(e.g. also check for "isn't an item"). Or invert the logic and call
itemExists before running op item delete (consistent with how put
checks existence), though that adds a round-trip.
Low
delete("item/field") silently deletes the entire item - onepassword.ts:412-445
parseSecretKey("my-item/password") returns item="my-item" and
field="password", but delete only uses parsed.item - the field is
discarded. This means delete("my-item/password") destroys the entire
my-item including all other fields. This is tested
(onepassword_test.ts:810) and appears intentional since op item delete
operates on whole items, but it is asymmetric with put("item/field")
which targets a specific field. A user who stores multiple fields
under one item could lose data by assuming delete is field-scoped.
Not blocking since this is a deliberate design choice with test
coverage, but worth calling out as an API surface surprise.
Verdict
PASS - The delete implementation is structurally sound, follows
existing patterns (URI rejection, account passthrough, error
classification), and has thorough test coverage. The idempotency guard
fragility (Medium 1) is a real gap between mock and production behavior
but does not cause data loss - it would surface as an unexpected error
on double-delete rather than silent corruption. Worth fixing before or
shortly after merge, but not blocking.