From cd1bd4d36311890657ad226be913d57b0a76332c Mon Sep 17 00:00:00 2001 From: Drug Date: Sat, 10 Oct 2026 22:32:52 +0800 Subject: [PATCH 1/7] feat(shares): copy share link with newly set password without persisting plaintext --- frontend/src/components/ShareModal.tsx | 47 ++++++++++++++---- frontend/src/i18n/coverageTranslations.ts | 16 +++++++ frontend/src/lib/shareCopySession.ts | 58 +++++++++++++++++++++++ frontend/src/types/index.ts | 2 + 4 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 frontend/src/lib/shareCopySession.ts diff --git a/frontend/src/components/ShareModal.tsx b/frontend/src/components/ShareModal.tsx index b47f64cbf..aa1776f58 100644 --- a/frontend/src/components/ShareModal.tsx +++ b/frontend/src/components/ShareModal.tsx @@ -10,6 +10,7 @@ import { Input } from "@/components/ui/input"; import { confirm } from "@/components/ui/confirm"; import { api } from "@/lib/api"; import { copyText } from "@/lib/clipboard"; +import { ShareCopySession, formatShareWithPassword } from "@/lib/shareCopySession"; import { buildPublicWebUrl, resolvePublicWebOrigin, @@ -52,6 +53,11 @@ export default function ShareModal({ noteId, noteTitle, initialShareId, onClose const [originSaving, setOriginSaving] = useState(false); const modalRef = useRef(null); const initialShareAppliedRef = useRef(null); + const copySessionRef = useRef(new ShareCopySession()); + + // Never persist plaintext share passwords in storage or across dialogs/notes. + useEffect(() => () => copySessionRef.current.clear(), []); + useEffect(() => { copySessionRef.current.clear(); }, [noteId]); const publicOrigin = resolvePublicWebOrigin({ runtimeOrigin: siteConfig.publicWebOrigin, @@ -155,19 +161,22 @@ export default function ShareModal({ noteId, noteTitle, initialShareId, onClose maxViews: parsedMax, }; if (editingId) { - await api.updateShare(editingId, { + const updated = await api.updateShare(editingId, { ...common, ...(password.trim() ? { password: password.trim() } : {}), }); + if (password.trim()) copySessionRef.current.remember(updated, password); + else if (!updated.hasPassword) copySessionRef.current.forget(updated.id); toast.success(t("shareUi.shareUpdated")); } else { - await api.createShare({ + const created = await api.createShare({ noteId, permission, password: password.trim() || undefined, expiresAt: common.expiresAt || undefined, maxViews: parsedMax || undefined, }); + if (password.trim()) copySessionRef.current.remember(created, password); if (publicOrigin.requiresAnonymousCheck) { toast.warning(t("shareUi.createdVerify")); } else { @@ -200,14 +209,34 @@ export default function ShareModal({ noteId, noteTitle, initialShareId, onClose } }; - const mutate = async (action: () => Promise, success: string) => { - try { await action(); toast.success(success); await loadShares(); } - catch (error: any) { toast.error(error?.message || t("shareUi.operationFailed")); } + const copyWithPassword = async (share: Share) => { + const knownPassword = copySessionRef.current.get(share); + if (!knownPassword) { + editShare(share); + toast.info(t("shareUi.passwordMustReset")); + return; + } + const text = formatShareWithPassword(noteTitle, shareUrl(share.shareToken), knownPassword, { + note: t("shareUi.copyNoteLabel"), + link: t("shareUi.copyLinkLabel"), + password: t("shareUi.copyPasswordLabel"), + }); + const ok = await copyText(text); + if (!ok) { toast.error(t("shareUi.copyFailed")); return; } + if (publicOrigin.requiresAnonymousCheck) toast.warning(t("shareUi.copiedVerify")); + else toast.success(t("shareUi.copiedWithPassword")); + }; + + const mutate = async (action: () => Promise, success: string): Promise => { + try { await action(); toast.success(success); await loadShares(); return true; } + catch (error: any) { toast.error(error?.message || t("shareUi.operationFailed")); return false; } }; const rotate = async (share: Share) => { if (!await confirm({ title: t("shareUi.rotateTitle"), description: t("shareUi.rotateWarning") })) return; - await mutate(() => api.updateShare(share.id, { rotateToken: true }), t("shareUi.rotated")); + if (await mutate(() => api.updateShare(share.id, { rotateToken: true }), t("shareUi.rotated"))) { + copySessionRef.current.forget(share.id); + } }; const resetViews = async (share: Share) => { if (!await confirm({ title: t("shareUi.resetTitle"), description: t("shareUi.resetDescription") })) return; @@ -215,7 +244,9 @@ export default function ShareModal({ noteId, noteTitle, initialShareId, onClose }; const remove = async (share: Share) => { if (!await confirm({ title: t("shareUi.deleteTitle"), description: t("shareUi.deleteDescription"), danger: true })) return; - await mutate(() => api.deleteShare(share.id), t("shareUi.deleted")); + if (await mutate(() => api.deleteShare(share.id), t("shareUi.deleted"))) { + copySessionRef.current.forget(share.id); + } }; const riskMessage = publicOrigin.isLikelyProtectedGateway @@ -290,7 +321,7 @@ export default function ShareModal({ noteId, noteTitle, initialShareId, onClose const url = shareUrl(share.shareToken); return
{permissionLabel(share.permission)}{active ? t("shareUi.active") : t("shareUi.inactive")}{share.hasPassword && {t("shareUi.password")}}

{url}

{t("shareUi.viewSessions", { total: share.viewCount || 0 })}{share.maxViews ? ` / ${share.maxViews}` : ""}{share.expiresAt ? t("shareUi.expiresOn", { date: new Date(share.expiresAt).toLocaleString(i18n.language) }) : ""}

-
+
{share.hasPassword && }
; })}} diff --git a/frontend/src/i18n/coverageTranslations.ts b/frontend/src/i18n/coverageTranslations.ts index f23540b15..693a7ba5a 100644 --- a/frontend/src/i18n/coverageTranslations.ts +++ b/frontend/src/i18n/coverageTranslations.ts @@ -375,6 +375,14 @@ export const zhCNCoverageTranslations = { "copyFailed": "复制失败,请手动复制", "copiedVerify": "链接已复制,请在无痕窗口、微信或未登录设备中验证", "copied": "分享链接已复制", + "copyWithPassword": "复制链接和密码", + "copyWithPasswordHint": "链接与密码将一起复制到剪贴板,请只发送给可信任的人", + "resetPasswordToCopy": "重设密码后复制", + "passwordMustReset": "无法找回原密码,请先在左侧重新设置访问密码并保存", + "copiedWithPassword": "已复制分享链接和访问密码,请注意安全发送", + "copyNoteLabel": "笔记:", + "copyLinkLabel": "分享链接:", + "copyPasswordLabel": "访问密码:", "operationFailed": "操作失败", "rotateTitle": "轮换分享链接?", "rotateWarning": "旧链接和旧密码访问令牌会立即失效,访问会话数会重置。", @@ -944,6 +952,14 @@ export const enCoverageTranslations = { "copyFailed": "Copy failed. Please copy manually", "copiedVerify": "Link copied. Verify it in a private window, WeChat or on a signed-out device.", "copied": "Share link copied", + "copyWithPassword": "Copy link and password", + "copyWithPasswordHint": "The link and password will be copied together. Share only with trusted recipients.", + "resetPasswordToCopy": "Reset password to copy", + "passwordMustReset": "The original password cannot be recovered. Set a new password on the left and save.", + "copiedWithPassword": "Copied link and password. Share securely.", + "copyNoteLabel": "Note: ", + "copyLinkLabel": "Share link: ", + "copyPasswordLabel": "Access password: ", "operationFailed": "Operation failed", "rotateTitle": "Regenerate share link?", "rotateWarning": "The old link and password access tokens will immediately stop working, and access sessions will reset.", diff --git a/frontend/src/lib/shareCopySession.ts b/frontend/src/lib/shareCopySession.ts new file mode 100644 index 000000000..0e850c931 --- /dev/null +++ b/frontend/src/lib/shareCopySession.ts @@ -0,0 +1,58 @@ +import type { Share } from "@/types"; + +/** + * Keep plaintext credentials only during the current share-dialog session. + * The API returns bcrypt hashes and NEVER supplies the original password. + */ +type ShareIdentity = Pick; +type SessionPassword = { + password: string; + token: string; + credentialVersion: number; +}; + +export class ShareCopySession { + private readonly known = new Map(); + + /** Call only after the create/reset-password request succeeds. */ + remember(share: ShareIdentity, submittedPassword: string): void { + const password = submittedPassword.trim(); + if (!share.hasPassword) { + this.forget(share.id); + return; + } + if (!password || !Number.isSafeInteger(share.credentialVersion)) return; + this.known.set(share.id, { + password, + token: share.shareToken, + credentialVersion: share.credentialVersion!, + }); + } + + get(share: ShareIdentity): string | null { + const saved = this.known.get(share.id); + if (!saved) return null; + if (!share.hasPassword || saved.token !== share.shareToken || + saved.credentialVersion !== share.credentialVersion) { + this.forget(share.id); + return null; + } + return saved.password; + } + + forget(shareId: string): void { this.known.delete(shareId); } + clear(): void { this.known.clear(); } +} + +export function formatShareWithPassword( + noteTitle: string, + url: string, + password: string, + labels: { note: string; link: string; password: string }, +): string { + return [ + `${labels.note}${noteTitle.replace(/[\r\n]+/g, " ").trim()}`, + `${labels.link}${url}`, + `${labels.password}${password}`, + ].join("\n"); +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 27cc6be7c..4742990af 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -749,6 +749,8 @@ export interface Share { shareType: string; permission: SharePermission; hasPassword: boolean; + /** Server-side credential epoch; lets an in-memory copy secret detect a reset. */ + credentialVersion?: number; expiresAt: string | null; maxViews: number | null; viewCount: number; From a9449b38c80539afdf8a31a2b4d474a6380ec629 Mon Sep 17 00:00:00 2001 From: Drug Date: Sat, 10 Oct 2026 22:34:08 +0800 Subject: [PATCH 2/7] test(shares): guard ephemeral copy-password lifetime and credential reset --- .../lib/__tests__/shareCopySession.test.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 frontend/src/lib/__tests__/shareCopySession.test.ts diff --git a/frontend/src/lib/__tests__/shareCopySession.test.ts b/frontend/src/lib/__tests__/shareCopySession.test.ts new file mode 100644 index 000000000..57a558421 --- /dev/null +++ b/frontend/src/lib/__tests__/shareCopySession.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; +import { ShareCopySession, formatShareWithPassword } from "@/lib/shareCopySession"; + +const protectedShare = { + id: "share-a", shareToken: "token-a", hasPassword: true, credentialVersion: 1, +}; +describe("share copy with access password (session only)", () => { + it("copies a stable human-readable text and trims only the submitted credential", () => { + const memory = new ShareCopySession(); + memory.remember(protectedShare, " abc123 "); + expect(memory.get(protectedShare)).toBe("abc123"); + expect(formatShareWithPassword("第一行\n第二行", "https://example.com/share/token-a", + memory.get(protectedShare)!, { note:"笔记:",link:"分享链接:",password:"访问密码:" })) + .toBe("笔记:第一行 第二行\n分享链接:https://example.com/share/token-a\n访问密码:abc123"); + }); + it("never retrieves an existing share password without a successful explicit input", () => { + const memory = new ShareCopySession(); + expect(memory.get(protectedShare)).toBeNull(); + memory.remember(protectedShare, ""); + expect(memory.get(protectedShare)).toBeNull(); + expect(memory.get({ ...protectedShare, id:"other-share" })).toBeNull(); + }); + it("invalidates when token or server credential version changes", () => { + const memory = new ShareCopySession(); + memory.remember(protectedShare, "abc123"); + expect(memory.get({ ...protectedShare, credentialVersion:2 })).toBeNull(); + expect(memory.get(protectedShare)).toBeNull(); + memory.remember(protectedShare, "abc123"); + expect(memory.get({ ...protectedShare, shareToken:"token-rotated" })).toBeNull(); + memory.remember(protectedShare, "abc123"); + expect(memory.get({ ...protectedShare, hasPassword:false })).toBeNull(); + }); + it("clears all secrets on dialog close and individual secrets on revoke", () => { + const memory = new ShareCopySession(); + const other={ ...protectedShare, id:"share-b" }; + memory.remember(protectedShare,"abc123"); + memory.remember(other,"def456"); + memory.forget(protectedShare.id); + expect(memory.get(protectedShare)).toBeNull(); + expect(memory.get(other)).toBe("def456"); + memory.clear(); + expect(memory.get(other)).toBeNull(); + }); + it("does not remember unverified or passwordless server responses", () => { + const memory = new ShareCopySession(); + memory.remember({ ...protectedShare, credentialVersion:undefined }, "abc123"); + expect(memory.get(protectedShare)).toBeNull(); + memory.remember({ ...protectedShare, hasPassword:false }, "abc123"); + expect(memory.get(protectedShare)).toBeNull(); + }); +}); From 0b4b59f2f3bfda555b614aaea7706ac36fcd2e03 Mon Sep 17 00:00:00 2001 From: Drug Date: Sat, 10 Oct 2026 22:35:12 +0800 Subject: [PATCH 3/7] test(shares): verify password copy appears only within successful modal session --- .../__tests__/shareModalPasswordCopy.test.tsx | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 frontend/src/lib/__tests__/shareModalPasswordCopy.test.tsx diff --git a/frontend/src/lib/__tests__/shareModalPasswordCopy.test.tsx b/frontend/src/lib/__tests__/shareModalPasswordCopy.test.tsx new file mode 100644 index 000000000..f5a36fdb1 --- /dev/null +++ b/frontend/src/lib/__tests__/shareModalPasswordCopy.test.tsx @@ -0,0 +1,123 @@ +// @vitest-environment jsdom +import React, { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { Share } from "@/types"; + +const mocks = vi.hoisted(() => ({ + list: vi.fn(), getMe: vi.fn(), create: vi.fn(), update: vi.fn(), copy: vi.fn(), + success: vi.fn(), warning: vi.fn(), info: vi.fn(), error: vi.fn(), +})); +vi.mock("@/lib/api", () => ({ api: { + getSharesByNote: mocks.list, getMe: mocks.getMe, + createShare: mocks.create, updateShare: mocks.update, +} })); +vi.mock("@/lib/clipboard", () => ({ copyText: mocks.copy })); +vi.mock("@/lib/toast", () => ({ toast: { + success: mocks.success, warning: mocks.warning, + info: mocks.info, error: mocks.error, +} })); +vi.mock("@/hooks/useSiteSettings", () => ({ useSiteSettings: () => ({ + siteConfig: { publicWebOrigin: "https://notes.example.com", publicWebOriginSource: "settings" }, + updatePublicWebOrigin: vi.fn(), +}) })); +vi.mock("@/lib/publicWebOrigin", () => ({ + resolvePublicWebOrigin: () => ({ + origin:"https://notes.example.com", source:"settings", requiresAnonymousCheck:false, + risk:"none", isLikelyProtectedGateway:false, + }), + buildPublicWebUrl: (path: string) => "https://notes.example.com" + path, +})); +vi.mock("framer-motion", () => ({ + AnimatePresence: ({ children }: { children: React.ReactNode }) => children, + motion: { div:"div" }, +})); +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, i18n: { language:"zh" } }), +})); + +import ShareModal from "@/components/ShareModal"; +(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; +let root: Root; +let host: HTMLDivElement; +let rows: Share[]; +const baseShare: Share = { + id: "share-1", noteId: "note-a", ownerId:"user-a", shareToken:"secret-link-token", + shareType:"link", permission:"view", hasPassword:true, credentialVersion:1, + expiresAt:null, maxViews:null, viewCount:0, isActive:1, createdAt:"now", updatedAt:"now", +}; +async function render() { + root = createRoot(host); + await act(async () => root.render( + undefined} />, + )); +} +function buttonContaining(fragment: string): HTMLButtonElement { + const btn = [...host.querySelectorAll("button")] + .find((item) => item.textContent?.includes(fragment)); + if (!btn) throw new Error("Cannot find button: " + fragment); + return btn; +} +async function click(btn: HTMLButtonElement) { + await act(async () => btn.dispatchEvent(new MouseEvent("click", { bubbles:true }))); +} +beforeEach(() => { + rows = []; + for (const mock of Object.values(mocks)) mock.mockReset(); + mocks.list.mockImplementation(async () => [...rows]); + mocks.getMe.mockResolvedValue({ role:"user" }); + mocks.copy.mockResolvedValue(true); + mocks.create.mockImplementation(async (body: { password?:string }) => { + const row = { ...baseShare, hasPassword:Boolean(body.password) }; + rows = [row]; + return row; + }); + mocks.update.mockImplementation(async (_id: string, body: { password?: string }) => { + const row = { ...baseShare, credentialVersion: 2, hasPassword:Boolean(body.password) }; + rows = [row]; + return row; + }); + host = document.createElement("div"); + document.body.appendChild(host); +}); +afterEach(() => { + if (root) act(() => root.unmount()); + host.remove(); +}); +describe("share password copy UI", () => { + it("keeps old passwords unrecoverable but retains copy-link and guides reset", async () => { + rows = [baseShare]; + await render(); + expect(buttonContaining("shareUi.copy")).toBeTruthy(); + expect(buttonContaining("shareUi.resetPasswordToCopy")).toBeTruthy(); + await click(buttonContaining("shareUi.resetPasswordToCopy")); + expect(mocks.copy).not.toHaveBeenCalled(); + expect(mocks.info).toHaveBeenCalledWith("shareUi.passwordMustReset"); + expect(host.textContent).toContain("shareUi.editShareSettings"); + await click(buttonContaining("shareUi.copy")); + expect(mocks.copy).toHaveBeenCalledWith("https://notes.example.com/share/secret-link-token"); + }); + + it("offers one-click link and password only after successful creation; loses it on reopen", async () => { + await render(); + const pw = host.querySelector('input[type="password"]') as HTMLInputElement; + expect(pw).toBeTruthy(); + const nativeSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!; + await act(async () => { + nativeSetter.call(pw, " abcd1234 "); + pw.dispatchEvent(new Event("input", { bubbles:true })); + }); + await click(buttonContaining("shareUi.createLink")); + expect(mocks.create).toHaveBeenCalledWith(expect.objectContaining({ password:"abcd1234" })); + await click(buttonContaining("shareUi.copyWithPassword")); + expect(mocks.copy).toHaveBeenLastCalledWith( + "shareUi.copyNoteLabel测试笔记\n" + + "shareUi.copyLinkLabelhttps://notes.example.com/share/secret-link-token\n" + + "shareUi.copyPasswordLabelabcd1234", + ); + await act(async () => root.unmount()); + await render(); + expect(buttonContaining("shareUi.resetPasswordToCopy")).toBeTruthy(); + expect(host.textContent).not.toContain("shareUi.copyWithPassword"); + }); +}); From cb0264024021225cc76080ad9f1ecb1a548ae26e Mon Sep 17 00:00:00 2001 From: Drug Date: Sat, 10 Oct 2026 22:36:25 +0800 Subject: [PATCH 4/7] ci(shares): gate password-copy session and modal integration tests --- .github/workflows/share-password-copy-ci.yml | 31 ++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/share-password-copy-ci.yml diff --git a/.github/workflows/share-password-copy-ci.yml b/.github/workflows/share-password-copy-ci.yml new file mode 100644 index 000000000..cb6a2a35b --- /dev/null +++ b/.github/workflows/share-password-copy-ci.yml @@ -0,0 +1,31 @@ +name: Share Password Copy CI + +on: + pull_request: + paths: + - "frontend/src/components/ShareModal.tsx" + - "frontend/src/lib/shareCopySession.ts" + - "frontend/src/lib/__tests__/shareCopySession.test.ts" + - "frontend/src/lib/__tests__/shareModalPasswordCopy.test.tsx" + - "frontend/src/i18n/coverageTranslations.ts" + - "frontend/src/types/index.ts" + - ".github/workflows/share-password-copy-ci.yml" + +jobs: + share-password-copy: + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + cache-dependency-path: frontend/package-lock.json + - run: npm ci + - name: Clipboard credential lifecycle and ShareModal UI + run: npm run test:run -- src/lib/__tests__/shareCopySession.test.ts src/lib/__tests__/shareModalPasswordCopy.test.tsx + - name: TypeScript + run: npx tsc -b From d4673e1aa7fb4a1b002e4250a625dfc712d496d0 Mon Sep 17 00:00:00 2001 From: Drug Date: Sat, 10 Oct 2026 22:37:07 +0800 Subject: [PATCH 5/7] test(shares): cover copying only the newly reset access password --- .../__tests__/shareModalPasswordCopy.test.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/frontend/src/lib/__tests__/shareModalPasswordCopy.test.tsx b/frontend/src/lib/__tests__/shareModalPasswordCopy.test.tsx index f5a36fdb1..883bce001 100644 --- a/frontend/src/lib/__tests__/shareModalPasswordCopy.test.tsx +++ b/frontend/src/lib/__tests__/shareModalPasswordCopy.test.tsx @@ -98,6 +98,25 @@ describe("share password copy UI", () => { expect(mocks.copy).toHaveBeenCalledWith("https://notes.example.com/share/secret-link-token"); }); + it("copies the newly reset password, never the unknown historical password", async () => { + rows = [baseShare]; + await render(); + await click(buttonContaining("shareUi.resetPasswordToCopy")); + const field = host.querySelector('input[type="password"]') as HTMLInputElement; + const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!; + await act(async () => { + setter.call(field, "newPass456"); + field.dispatchEvent(new Event("input", { bubbles:true })); + }); + await click(buttonContaining("shareUi.saveSettings")); + expect(mocks.update).toHaveBeenCalledWith("share-1", + expect.objectContaining({ password:"newPass456" })); + await click(buttonContaining("shareUi.copyWithPassword")); + expect(mocks.copy).toHaveBeenLastCalledWith(expect.stringContaining( + "shareUi.copyPasswordLabelnewPass456", + )); + }); + it("offers one-click link and password only after successful creation; loses it on reopen", async () => { await render(); const pw = host.querySelector('input[type="password"]') as HTMLInputElement; From 2b0e7dc328cc1d235d98cad1a0bbf495559737e4 Mon Sep 17 00:00:00 2001 From: Drug Date: Sat, 10 Oct 2026 22:39:11 +0800 Subject: [PATCH 6/7] docs(shares): explain safe copy-link-and-password behavior --- docs/tutorials/sharing.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/sharing.md b/docs/tutorials/sharing.md index c75f55e87..4afa6bc6c 100644 --- a/docs/tutorials/sharing.md +++ b/docs/tutorials/sharing.md @@ -45,7 +45,9 @@ nowen-note 支持 4 档分享权限: ## 分享链接 -配置完成后,点击「复制链接」获取分享 URL。 +配置完成后,点击「复制链接」获取分享 URL。对于**本次弹窗中新建或重设过访问密码**的分享,还可以点击「复制链接和密码」,将笔记标题、链接和访问密码一起复制给受信任的接收方。 + +历史密码仅以哈希方式保存在服务端,无法取回;重新打开分享弹窗后,如果需要连密码一起复制,请先重新设置访问密码。复制到剪贴板的内容包含访问凭据,请谨慎发送。 将链接发送给需要查看的人,他们不需要登录就能访问(除非设置了「需登录」权限)。 From fec2ff305d904ddddbfa1fb0afee08e084720be2 Mon Sep 17 00:00:00 2001 From: Drug Date: Sun, 11 Oct 2026 05:29:30 +0800 Subject: [PATCH 7/7] fix(shares): handle password-copy errors without new any lint violations --- frontend/src/components/ShareModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/ShareModal.tsx b/frontend/src/components/ShareModal.tsx index aa1776f58..775928efd 100644 --- a/frontend/src/components/ShareModal.tsx +++ b/frontend/src/components/ShareModal.tsx @@ -229,7 +229,7 @@ export default function ShareModal({ noteId, noteTitle, initialShareId, onClose const mutate = async (action: () => Promise, success: string): Promise => { try { await action(); toast.success(success); await loadShares(); return true; } - catch (error: any) { toast.error(error?.message || t("shareUi.operationFailed")); return false; } + catch (error: unknown) { toast.error(error instanceof Error ? error.message : t("shareUi.operationFailed")); return false; } }; const rotate = async (share: Share) => {