From 9d832554266b442e47bffa3a7eeb6a595c25602d Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 8 May 2026 14:05:34 +0900 Subject: [PATCH 1/8] feat(shared): add tolerant-fsync utility for synced-folder filesystems Adds isToleratedFsyncError, tolerantFsync (async, FileHandle), and tolerantFsyncSync (sync, fd) helpers that swallow filesystem-limitation errors during fsync (EPERM, EACCES, ENOTSUP, EINVAL) while still propagating real errors (EIO, ENOSPC, EBADF, etc.). Synced folders like iCloud Drive, OneDrive, and antivirus-locked files reject fsync with EPERM even though the underlying write+rename succeeded; for the runtime data this codebase persists, losing the durability hint is acceptable in exchange for not blocking the operation entirely. The helper is intentionally not barrel-exported (consumers import the file directly), matching the existing convention for write-file-atomically. --- src/shared/tolerant-fsync.test.ts | 135 ++++++++++++++++++++++++++++++ src/shared/tolerant-fsync.ts | 52 ++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 src/shared/tolerant-fsync.test.ts create mode 100644 src/shared/tolerant-fsync.ts diff --git a/src/shared/tolerant-fsync.test.ts b/src/shared/tolerant-fsync.test.ts new file mode 100644 index 000000000..f7be152dd --- /dev/null +++ b/src/shared/tolerant-fsync.test.ts @@ -0,0 +1,135 @@ +import { describe, expect, it } from "bun:test" +import { fsyncSync } from "node:fs" +import type { FileHandle } from "node:fs/promises" + +import { isToleratedFsyncError, tolerantFsync, tolerantFsyncSync } from "./tolerant-fsync" + +function makeFsError(code: string, message?: string): NodeJS.ErrnoException { + const error = new Error(message ?? `${code}: simulated`) as NodeJS.ErrnoException + error.code = code + return error +} + +function fakeHandleWithSyncError(error: NodeJS.ErrnoException): FileHandle { + return { + sync: async () => { + throw error + }, + } as FileHandle +} + +describe("isToleratedFsyncError", () => { + it("#given EPERM error #when checked #then returns true", () => { + expect(isToleratedFsyncError(makeFsError("EPERM"))).toBe(true) + }) + + it("#given EACCES error #when checked #then returns true", () => { + expect(isToleratedFsyncError(makeFsError("EACCES"))).toBe(true) + }) + + it("#given ENOTSUP error #when checked #then returns true", () => { + expect(isToleratedFsyncError(makeFsError("ENOTSUP"))).toBe(true) + }) + + it("#given EINVAL error #when checked #then returns true", () => { + expect(isToleratedFsyncError(makeFsError("EINVAL"))).toBe(true) + }) + + it("#given EIO error #when checked #then returns false", () => { + expect(isToleratedFsyncError(makeFsError("EIO"))).toBe(false) + }) + + it("#given ENOSPC error (disk full) #when checked #then returns false", () => { + expect(isToleratedFsyncError(makeFsError("ENOSPC"))).toBe(false) + }) + + it("#given EBADF error (bad fd) #when checked #then returns false", () => { + expect(isToleratedFsyncError(makeFsError("EBADF"))).toBe(false) + }) + + it("#given non-Error value #when checked #then returns false", () => { + expect(isToleratedFsyncError("EPERM string")).toBe(false) + expect(isToleratedFsyncError(null)).toBe(false) + expect(isToleratedFsyncError(undefined)).toBe(false) + expect(isToleratedFsyncError({ code: "EPERM" })).toBe(false) + }) + + it("#given Error without code #when checked #then returns false", () => { + expect(isToleratedFsyncError(new Error("no code"))).toBe(false) + }) +}) + +describe("tolerantFsync (async)", () => { + it("#given fsync throws EPERM #when called #then resolves without throwing", async () => { + const handle = fakeHandleWithSyncError(makeFsError("EPERM", "operation not permitted, fsync")) + await expect(tolerantFsync(handle, "test:async-eperm")).resolves.toBeUndefined() + }) + + it("#given fsync throws EACCES #when called #then resolves without throwing", async () => { + const handle = fakeHandleWithSyncError(makeFsError("EACCES")) + await expect(tolerantFsync(handle, "test:async-eacces")).resolves.toBeUndefined() + }) + + it("#given fsync throws ENOTSUP #when called #then resolves without throwing", async () => { + const handle = fakeHandleWithSyncError(makeFsError("ENOTSUP")) + await expect(tolerantFsync(handle, "test:async-enotsup")).resolves.toBeUndefined() + }) + + it("#given fsync throws EINVAL #when called #then resolves without throwing", async () => { + const handle = fakeHandleWithSyncError(makeFsError("EINVAL")) + await expect(tolerantFsync(handle, "test:async-einval")).resolves.toBeUndefined() + }) + + it("#given fsync throws EIO #when called #then propagates the error", async () => { + const handle = fakeHandleWithSyncError(makeFsError("EIO")) + await expect(tolerantFsync(handle, "test:async-eio")).rejects.toThrow("EIO: simulated") + }) + + it("#given fsync throws ENOSPC #when called #then propagates the error", async () => { + const handle = fakeHandleWithSyncError(makeFsError("ENOSPC")) + await expect(tolerantFsync(handle, "test:async-enospc")).rejects.toThrow("ENOSPC: simulated") + }) + + it("#given fsync succeeds #when called #then resolves and sync was invoked", async () => { + let syncCalled = false + const handle = { + sync: async () => { + syncCalled = true + }, + } as FileHandle + await tolerantFsync(handle, "test:async-success") + expect(syncCalled).toBe(true) + }) +}) + +describe("tolerantFsyncSync (synchronous)", () => { + it("#given fsyncSync throws EPERM #when called #then returns without throwing", () => { + const fakeFsync = ((_fileDescriptor: number): void => { + throw makeFsError("EPERM", "operation not permitted, fsync") + }) as typeof fsyncSync + expect(() => tolerantFsyncSync(123, "test:sync-eperm", fakeFsync)).not.toThrow() + }) + + it("#given fsyncSync throws EACCES #when called #then returns without throwing", () => { + const fakeFsync = ((_fileDescriptor: number): void => { + throw makeFsError("EACCES") + }) as typeof fsyncSync + expect(() => tolerantFsyncSync(123, "test:sync-eacces", fakeFsync)).not.toThrow() + }) + + it("#given fsyncSync throws EIO #when called #then propagates the error", () => { + const fakeFsync = ((_fileDescriptor: number): void => { + throw makeFsError("EIO") + }) as typeof fsyncSync + expect(() => tolerantFsyncSync(123, "test:sync-eio", fakeFsync)).toThrow("EIO: simulated") + }) + + it("#given fsyncSync succeeds #when called #then returns and impl was invoked", () => { + let called = false + const fakeFsync = ((_fileDescriptor: number): void => { + called = true + }) as typeof fsyncSync + tolerantFsyncSync(123, "test:sync-success", fakeFsync) + expect(called).toBe(true) + }) +}) diff --git a/src/shared/tolerant-fsync.ts b/src/shared/tolerant-fsync.ts new file mode 100644 index 000000000..00612ee3c --- /dev/null +++ b/src/shared/tolerant-fsync.ts @@ -0,0 +1,52 @@ +import { fsyncSync } from "node:fs" +import type { FileHandle } from "node:fs/promises" + +import { log } from "./logger" + +const TOLERATED_FSYNC_CODES: ReadonlySet = new Set([ + "EPERM", + "EACCES", + "ENOTSUP", + "EINVAL", +]) + +export function isToleratedFsyncError(error: unknown): boolean { + if (!(error instanceof Error)) return false + const code = (error as NodeJS.ErrnoException).code + return code !== undefined && TOLERATED_FSYNC_CODES.has(code) +} + +export async function tolerantFsync( + fileHandle: FileHandle, + contextLabel: string, +): Promise { + try { + await fileHandle.sync() + } catch (error) { + if (!isToleratedFsyncError(error)) throw error + log("fsync skipped due to filesystem limitation", { + event: "fsync-skipped", + contextLabel, + code: (error as NodeJS.ErrnoException).code, + message: error instanceof Error ? error.message : String(error), + }) + } +} + +export function tolerantFsyncSync( + fileDescriptor: number, + contextLabel: string, + fsyncImpl: typeof fsyncSync = fsyncSync, +): void { + try { + fsyncImpl(fileDescriptor) + } catch (error) { + if (!isToleratedFsyncError(error)) throw error + log("fsync skipped due to filesystem limitation", { + event: "fsync-skipped", + contextLabel, + code: (error as NodeJS.ErrnoException).code, + message: error instanceof Error ? error.message : String(error), + }) + } +} From c69d6bd964019c2e254a838aa96562aab483e8d3 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 8 May 2026 14:05:46 +0900 Subject: [PATCH 2/8] fix(team-mode): tolerate EPERM during fsync in atomicWrite and acquireLock Replaces direct fileHandle.sync() calls in acquireLock and atomicWrite with tolerantFsync. Users on iCloud Drive / OneDrive / Desktop sync folders were hitting 'EPERM: operation not permitted, fsync' during team_create, which propagated up and aborted the entire team_create flow even though the actual write+rename had succeeded. Reported on Discord (omo 4.0.0, opencode desktop 1.14.41, project on synced Desktop). atomicity is preserved by the temp-file rename; only the durability hint is now best-effort on filesystems that disallow fsync. --- src/features/team-mode/team-state-store/locks.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/features/team-mode/team-state-store/locks.ts b/src/features/team-mode/team-state-store/locks.ts index 0fca98494..2f7a4d4a0 100644 --- a/src/features/team-mode/team-state-store/locks.ts +++ b/src/features/team-mode/team-state-store/locks.ts @@ -1,6 +1,8 @@ import { randomUUID } from "node:crypto" import { open, readFile, rename, rm, unlink, writeFile } from "node:fs/promises" +import { tolerantFsync } from "../../../shared/tolerant-fsync" + type LockOptions = { staleAfterMs?: number ownerTag?: string @@ -51,7 +53,7 @@ async function acquireLock(lockPath: string, ownerTag: string, staleAfterMs: num const fileHandle = await open(lockPath, "wx") try { await fileHandle.writeFile(buildOwnerContent(ownerTag)) - await fileHandle.sync() + await tolerantFsync(fileHandle, `acquireLock:${lockPath}`) } finally { await fileHandle.close() } @@ -116,7 +118,7 @@ export async function atomicWrite( await writeFile(tmpPath, content) const fileHandle = await open(tmpPath, "r") try { - await fileHandle.sync() + await tolerantFsync(fileHandle, `atomicWrite:${filePath}`) } finally { await fileHandle.close() } From 7735b2abd50591a860632172e0378359c527d725 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 8 May 2026 14:05:57 +0900 Subject: [PATCH 3/8] fix(shared): tolerate EPERM during fsync in writeFileAtomically MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces fsyncSync(tempFileDescriptor) with tolerantFsyncSync, allowing EPERM/EACCES/ENOTSUP/EINVAL during fsync while still propagating real errors. Adds an optional deps.fsyncSync injection point used solely by the new EPERM tolerance regression tests. Without this fix, plugin startup itself can fail on synced folders because writeFileAtomically is used by config migrations and posthog activity state — the same EPERM-on-fsync failure pattern reported for team_create. --- src/shared/write-file-atomically.test.ts | 35 ++++++++++++++++++++++++ src/shared/write-file-atomically.ts | 23 ++++++++++++---- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/shared/write-file-atomically.test.ts b/src/shared/write-file-atomically.test.ts index ce4a5c8f9..2c13cd2ff 100644 --- a/src/shared/write-file-atomically.test.ts +++ b/src/shared/write-file-atomically.test.ts @@ -51,4 +51,39 @@ describe("writeFileAtomically", () => { // when/then expect(() => writeFileAtomically(filePath, "content")).toThrow() }) + + it("#given fsync fails with EPERM (synced folder) #when writeFileAtomically called #then write succeeds", () => { + // given + const filePath = join(testDir, "synced-folder.txt") + const content = "content from a synced folder where fsync is rejected" + + // when + writeFileAtomically(filePath, content, { + fsyncSync: () => { + const error = new Error("EPERM: operation not permitted, fsync") as NodeJS.ErrnoException + error.code = "EPERM" + throw error + }, + }) + + // then + expect(existsSync(filePath)).toBe(true) + expect(readFileSync(filePath, "utf-8")).toBe(content) + }) + + it("#given fsync fails with EIO (real I/O error) #when writeFileAtomically called #then propagates the error", () => { + // given + const filePath = join(testDir, "io-error.txt") + + // when/then + expect(() => + writeFileAtomically(filePath, "content", { + fsyncSync: () => { + const error = new Error("EIO: input/output error") as NodeJS.ErrnoException + error.code = "EIO" + throw error + }, + }), + ).toThrow("EIO") + }) }) diff --git a/src/shared/write-file-atomically.ts b/src/shared/write-file-atomically.ts index 9e9f123bc..09ce5b7d5 100644 --- a/src/shared/write-file-atomically.ts +++ b/src/shared/write-file-atomically.ts @@ -1,11 +1,24 @@ -import { closeSync, fsyncSync, openSync, renameSync, unlinkSync, writeFileSync } from "node:fs" +import { + closeSync, + type fsyncSync as FsyncSync, + openSync, + renameSync, + unlinkSync, + writeFileSync, +} from "node:fs" -export function writeFileAtomically(filePath: string, content: string): void { - const tempPath = `${filePath}.tmp` - writeFileSync(tempPath, content, "utf-8") +import { tolerantFsyncSync } from "./tolerant-fsync" + +export function writeFileAtomically( + filePath: string, + content: string, + deps: { fsyncSync?: typeof FsyncSync } = {}, +): void { + const tempPath = `${filePath}.tmp` + writeFileSync(tempPath, content, "utf-8") const tempFileDescriptor = openSync(tempPath, "r") try { - fsyncSync(tempFileDescriptor) + tolerantFsyncSync(tempFileDescriptor, `writeFileAtomically:${filePath}`, deps.fsyncSync) } finally { closeSync(tempFileDescriptor) } From 7fe197d8f1efcd3c330c5df0a8d3287c02c77e24 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 8 May 2026 14:57:58 +0900 Subject: [PATCH 4/8] chore(workflows): trigger web-deploy on dev push too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRs land on `dev` (master is blocked by `block-master-pr`), but the deploy workflow only listened to `master` pushes — so #3853's web/ dependency bumps merged to dev with no Cloudflare deployment ever running. Add `dev` to the push branches list. The existing `paths` filter keeps the deploy from firing on non-web changes, and `workflow_dispatch` is preserved as the manual fallback. --- .github/workflows/web-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/web-deploy.yml b/.github/workflows/web-deploy.yml index 9bee30cf2..a8a10ba15 100644 --- a/.github/workflows/web-deploy.yml +++ b/.github/workflows/web-deploy.yml @@ -8,7 +8,7 @@ on: required: false default: "" push: - branches: [master] + branches: [master, dev] paths: - "web/**" - ".github/workflows/web-deploy.yml" From 20ae3f8ba5fe57201f43fd8716faf747f87b9cf0 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 8 May 2026 15:08:05 +0900 Subject: [PATCH 5/8] feat(shared): add fsync-skip tracker and path-environment classifier Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/shared/classify-path-environment.test.ts | 56 +++++++++++ src/shared/classify-path-environment.ts | 68 +++++++++++++ src/shared/fsync-skip-tracker.test.ts | 100 +++++++++++++++++++ src/shared/fsync-skip-tracker.ts | 42 ++++++++ 4 files changed, 266 insertions(+) create mode 100644 src/shared/classify-path-environment.test.ts create mode 100644 src/shared/classify-path-environment.ts create mode 100644 src/shared/fsync-skip-tracker.test.ts create mode 100644 src/shared/fsync-skip-tracker.ts diff --git a/src/shared/classify-path-environment.test.ts b/src/shared/classify-path-environment.test.ts new file mode 100644 index 000000000..0fc45c4b7 --- /dev/null +++ b/src/shared/classify-path-environment.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "bun:test" + +import { + classifyPathEnvironment, + describePathClassification, +} from "./classify-path-environment" + +describe("classifyPathEnvironment", () => { + it("classifies macOS iCloud path as icloud", () => { + expect( + classifyPathEnvironment( + "/Users/x/Library/Mobile Documents/com~apple~CloudDocs/project/file.txt", + ), + ).toBe("icloud") + }) + + it("classifies OneDrive path on unix style", () => { + expect(classifyPathEnvironment("/Users/x/OneDrive/foo")).toBe("onedrive") + }) + + it("classifies OneDrive path on windows style", () => { + expect(classifyPathEnvironment("C:\\Users\\x\\OneDrive\\foo")).toBe("onedrive") + }) + + it("classifies macOS Desktop path as desktop-sync", () => { + expect(classifyPathEnvironment("/Users/x/Desktop/foo")).toBe("desktop-sync") + }) + + it("classifies /Volumes path as network-drive", () => { + expect(classifyPathEnvironment("/Volumes/NetworkShare/foo")).toBe("network-drive") + }) + + it("classifies random path as unknown", () => { + expect(classifyPathEnvironment("/tmp/foo")).toBe("unknown") + }) + + it("classifies empty string as unknown", () => { + expect(classifyPathEnvironment("")).toBe("unknown") + }) + + it("matches OneDrive case-insensitively", () => { + expect(classifyPathEnvironment("/Users/x/oNeDrIvE/foo")).toBe("onedrive") + }) +}) + +describe("describePathClassification", () => { + it("returns human-readable descriptions", () => { + expect(describePathClassification("icloud")).toBe("iCloud Drive") + expect(describePathClassification("onedrive")).toBe("OneDrive") + expect(describePathClassification("desktop-sync")).toBe("Desktop sync (macOS)") + expect(describePathClassification("network-drive")).toBe("Network drive") + expect(describePathClassification("unknown")).toBe( + "filesystem that does not support fsync", + ) + }) +}) diff --git a/src/shared/classify-path-environment.ts b/src/shared/classify-path-environment.ts new file mode 100644 index 000000000..fe2974d54 --- /dev/null +++ b/src/shared/classify-path-environment.ts @@ -0,0 +1,68 @@ +import { homedir } from "node:os" +import path from "node:path" + +export type PathClassification = + | "icloud" + | "onedrive" + | "desktop-sync" + | "network-drive" + | "unknown" + +function normalizeInputPath(absolutePath: string): string { + return absolutePath.replaceAll("\\", "/") +} + +function isUnderPath(normalizedPath: string, normalizedParentPath: string): boolean { + return normalizedPath === normalizedParentPath || normalizedPath.startsWith(`${normalizedParentPath}/`) +} + +export function classifyPathEnvironment(absolutePath: string): PathClassification { + if (absolutePath.length === 0) return "unknown" + + const normalizedPath = normalizeInputPath(absolutePath) + const lowercasePath = normalizedPath.toLowerCase() + if (lowercasePath.includes("/onedrive") || lowercasePath.includes("/onedrive/")) { + return "onedrive" + } + + if (normalizedPath.includes("/Library/Mobile Documents/")) { + return "icloud" + } + + if (isUnderPath(normalizedPath, "/Volumes")) { + return "network-drive" + } + + if ( + normalizedPath.startsWith("/Users/") + && (normalizedPath.includes("/Desktop/") || normalizedPath.endsWith("/Desktop") + || normalizedPath.includes("/Documents/") || normalizedPath.endsWith("/Documents")) + ) { + return "desktop-sync" + } + + const normalizedHome = normalizeInputPath(homedir()) + const desktopPath = normalizeInputPath(path.join(normalizedHome, "Desktop")) + const documentsPath = normalizeInputPath(path.join(normalizedHome, "Documents")) + + if (isUnderPath(normalizedPath, desktopPath) || isUnderPath(normalizedPath, documentsPath)) { + return "desktop-sync" + } + + return "unknown" +} + +export function describePathClassification(pathClassification: PathClassification): string { + switch (pathClassification) { + case "icloud": + return "iCloud Drive" + case "onedrive": + return "OneDrive" + case "desktop-sync": + return "Desktop sync (macOS)" + case "network-drive": + return "Network drive" + case "unknown": + return "filesystem that does not support fsync" + } +} diff --git a/src/shared/fsync-skip-tracker.test.ts b/src/shared/fsync-skip-tracker.test.ts new file mode 100644 index 000000000..897e5efe8 --- /dev/null +++ b/src/shared/fsync-skip-tracker.test.ts @@ -0,0 +1,100 @@ +import { beforeEach, describe, expect, it } from "bun:test" + +import { + clearAllSkips, + drainSkipsAfter, + recordFsyncSkip, +} from "./fsync-skip-tracker" + +type PathClassification = + | "icloud" + | "onedrive" + | "desktop-sync" + | "network-drive" + | "unknown" + +function recordSkip(index: number, pathClassification: PathClassification = "unknown"): void { + recordFsyncSkip({ + filePath: `/tmp/file-${index}.txt`, + contextLabel: `atomicWrite:/tmp/file-${index}.txt`, + errorCode: "EPERM", + message: "operation not permitted", + pathClassification, + }) +} + +describe("fsync-skip-tracker", () => { + beforeEach(() => { + clearAllSkips() + }) + + it("recordFsyncSkip adds entry with timestamp", () => { + const before = Date.now() + recordSkip(1) + const entries = drainSkipsAfter(0) + + expect(entries).toHaveLength(1) + expect(entries[0]?.filePath).toBe("/tmp/file-1.txt") + expect(entries[0]?.timestamp).toBeGreaterThanOrEqual(before) + }) + + it("drainSkipsAfter(timestamp) returns entries strictly after the timestamp", async () => { + recordSkip(1) + const firstTimestamp = Date.now() + + await Bun.sleep(2) + + recordSkip(2) + const drained = drainSkipsAfter(firstTimestamp) + expect(drained).toHaveLength(1) + expect(drained[0]?.filePath).toBe("/tmp/file-2.txt") + }) + + it("drainSkipsAfter removes drained entries from buffer", () => { + recordSkip(1) + recordSkip(2) + + const drained = drainSkipsAfter(0) + expect(drained).toHaveLength(2) + expect(drainSkipsAfter(0)).toEqual([]) + }) + + it("buffer is bounded to max 200 entries and drops oldest on overflow", () => { + for (let index = 1; index <= 205; index += 1) { + recordSkip(index) + } + + const drained = drainSkipsAfter(0) + expect(drained).toHaveLength(200) + expect(drained[0]?.filePath).toBe("/tmp/file-6.txt") + expect(drained[199]?.filePath).toBe("/tmp/file-205.txt") + }) + + it("multiple records with same path are kept", () => { + recordSkip(1) + recordFsyncSkip({ + filePath: "/tmp/file-1.txt", + contextLabel: "acquireLock:/tmp/file-1.txt", + errorCode: "EPERM", + message: "second", + pathClassification: "unknown", + }) + + const drained = drainSkipsAfter(0) + expect(drained).toHaveLength(2) + expect(drained[0]?.filePath).toBe("/tmp/file-1.txt") + expect(drained[1]?.filePath).toBe("/tmp/file-1.txt") + }) + + it("drainSkipsAfter(0) returns all entries", () => { + recordSkip(1) + recordSkip(2) + + const drained = drainSkipsAfter(0) + expect(drained).toHaveLength(2) + }) + + it("empty buffer returns empty array", () => { + expect(drainSkipsAfter(0)).toEqual([]) + }) +}) diff --git a/src/shared/fsync-skip-tracker.ts b/src/shared/fsync-skip-tracker.ts new file mode 100644 index 000000000..3ee7a7125 --- /dev/null +++ b/src/shared/fsync-skip-tracker.ts @@ -0,0 +1,42 @@ +import type { PathClassification } from "./classify-path-environment" + +export type FsyncSkipEntry = { + filePath: string + contextLabel: string + errorCode: string + message: string + pathClassification: PathClassification + timestamp: number +} + +const MAX_SKIPS = 200 +const fsyncSkips: FsyncSkipEntry[] = [] + +export function recordFsyncSkip(entry: Omit): void { + fsyncSkips.push({ ...entry, timestamp: Date.now() }) + + if (fsyncSkips.length > MAX_SKIPS) { + fsyncSkips.splice(0, fsyncSkips.length - MAX_SKIPS) + } +} + +export function drainSkipsAfter(timestampMs: number): FsyncSkipEntry[] { + const drainedEntries: FsyncSkipEntry[] = [] + const retainedEntries: FsyncSkipEntry[] = [] + + for (const entry of fsyncSkips) { + if (entry.timestamp > timestampMs) { + drainedEntries.push(entry) + continue + } + + retainedEntries.push(entry) + } + + fsyncSkips.splice(0, fsyncSkips.length, ...retainedEntries) + return drainedEntries +} + +export function clearAllSkips(): void { + fsyncSkips.length = 0 +} From 6b69505940e51e43e91a7bd6af0bb372dca57773 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 8 May 2026 15:08:19 +0900 Subject: [PATCH 6/8] feat(shared): wire tolerantFsync to record skips with path classification Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/shared/tolerant-fsync.test.ts | 25 ++++++++++++++++++- src/shared/tolerant-fsync.ts | 41 ++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/shared/tolerant-fsync.test.ts b/src/shared/tolerant-fsync.test.ts index f7be152dd..0c785ec2e 100644 --- a/src/shared/tolerant-fsync.test.ts +++ b/src/shared/tolerant-fsync.test.ts @@ -1,7 +1,8 @@ -import { describe, expect, it } from "bun:test" +import { beforeEach, describe, expect, it } from "bun:test" import { fsyncSync } from "node:fs" import type { FileHandle } from "node:fs/promises" +import { clearAllSkips, drainSkipsAfter } from "./fsync-skip-tracker" import { isToleratedFsyncError, tolerantFsync, tolerantFsyncSync } from "./tolerant-fsync" function makeFsError(code: string, message?: string): NodeJS.ErrnoException { @@ -60,6 +61,10 @@ describe("isToleratedFsyncError", () => { }) describe("tolerantFsync (async)", () => { + beforeEach(() => { + clearAllSkips() + }) + it("#given fsync throws EPERM #when called #then resolves without throwing", async () => { const handle = fakeHandleWithSyncError(makeFsError("EPERM", "operation not permitted, fsync")) await expect(tolerantFsync(handle, "test:async-eperm")).resolves.toBeUndefined() @@ -100,6 +105,24 @@ describe("tolerantFsync (async)", () => { await tolerantFsync(handle, "test:async-success") expect(syncCalled).toBe(true) }) + + it("#given fsync throws EPERM #when called #then tracker records one skip", async () => { + const handle = fakeHandleWithSyncError(makeFsError("EPERM", "operation not permitted, fsync")) + + await tolerantFsync(handle, "atomicWrite:/Users/x/Library/Mobile Documents/com~apple~CloudDocs/file.txt") + + const entries = drainSkipsAfter(0) + expect(entries).toHaveLength(1) + expect(entries[0]?.errorCode).toBe("EPERM") + }) + + it("#given fsync throws EIO #when called #then tracker remains empty", async () => { + const handle = fakeHandleWithSyncError(makeFsError("EIO")) + + await expect(tolerantFsync(handle, "atomicWrite:/tmp/file.txt")).rejects.toThrow("EIO: simulated") + + expect(drainSkipsAfter(0)).toHaveLength(0) + }) }) describe("tolerantFsyncSync (synchronous)", () => { diff --git a/src/shared/tolerant-fsync.ts b/src/shared/tolerant-fsync.ts index 00612ee3c..e47b791b5 100644 --- a/src/shared/tolerant-fsync.ts +++ b/src/shared/tolerant-fsync.ts @@ -1,6 +1,8 @@ import { fsyncSync } from "node:fs" import type { FileHandle } from "node:fs/promises" +import { classifyPathEnvironment } from "./classify-path-environment" +import { recordFsyncSkip } from "./fsync-skip-tracker" import { log } from "./logger" const TOLERATED_FSYNC_CODES: ReadonlySet = new Set([ @@ -16,6 +18,13 @@ export function isToleratedFsyncError(error: unknown): boolean { return code !== undefined && TOLERATED_FSYNC_CODES.has(code) } +function extractPathFromContextLabel(contextLabel: string): string { + const separatorIndex = contextLabel.indexOf(":") + if (separatorIndex < 0) return contextLabel + + return contextLabel.slice(separatorIndex + 1) +} + export async function tolerantFsync( fileHandle: FileHandle, contextLabel: string, @@ -24,11 +33,23 @@ export async function tolerantFsync( await fileHandle.sync() } catch (error) { if (!isToleratedFsyncError(error)) throw error + const errorCode = (error as NodeJS.ErrnoException).code ?? "UNKNOWN" + const message = error instanceof Error ? error.message : String(error) + const filePath = extractPathFromContextLabel(contextLabel) + log("fsync skipped due to filesystem limitation", { event: "fsync-skipped", contextLabel, - code: (error as NodeJS.ErrnoException).code, - message: error instanceof Error ? error.message : String(error), + code: errorCode, + message, + }) + + recordFsyncSkip({ + filePath, + contextLabel, + errorCode, + message, + pathClassification: classifyPathEnvironment(filePath), }) } } @@ -42,11 +63,23 @@ export function tolerantFsyncSync( fsyncImpl(fileDescriptor) } catch (error) { if (!isToleratedFsyncError(error)) throw error + const errorCode = (error as NodeJS.ErrnoException).code ?? "UNKNOWN" + const message = error instanceof Error ? error.message : String(error) + const filePath = extractPathFromContextLabel(contextLabel) + log("fsync skipped due to filesystem limitation", { event: "fsync-skipped", contextLabel, - code: (error as NodeJS.ErrnoException).code, - message: error instanceof Error ? error.message : String(error), + code: errorCode, + message, + }) + + recordFsyncSkip({ + filePath, + contextLabel, + errorCode, + message, + pathClassification: classifyPathEnvironment(filePath), }) } } From 43b052955755996a722871f97f40cfc06cabdab5 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 8 May 2026 15:08:34 +0900 Subject: [PATCH 7/8] feat(hooks): surface fsync-skip warnings to AI agent via tool output Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/config/schema/hooks.ts | 1 + src/hooks/fsync-skip-warning/index.test.ts | 98 +++++++++++++++++++ src/hooks/fsync-skip-warning/index.ts | 50 ++++++++++ src/hooks/index.ts | 1 + src/plugin/hooks/create-tool-guard-hooks.ts | 7 ++ src/plugin/tool-execute-after.ts | 1 + src/plugin/tool-execute-before.ts | 5 +- .../fsync-skip-warning-formatter.test.ts | 78 +++++++++++++++ src/shared/fsync-skip-warning-formatter.ts | 61 ++++++++++++ 9 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 src/hooks/fsync-skip-warning/index.test.ts create mode 100644 src/hooks/fsync-skip-warning/index.ts create mode 100644 src/shared/fsync-skip-warning-formatter.test.ts create mode 100644 src/shared/fsync-skip-warning-formatter.ts diff --git a/src/config/schema/hooks.ts b/src/config/schema/hooks.ts index 80e4c71dd..641825da1 100644 --- a/src/config/schema/hooks.ts +++ b/src/config/schema/hooks.ts @@ -55,6 +55,7 @@ export const HookNameSchema = z.enum([ "read-image-resizer", "todo-description-override", "webfetch-redirect-guard", + "fsync-skip-warning", "legacy-plugin-toast", ]) diff --git a/src/hooks/fsync-skip-warning/index.test.ts b/src/hooks/fsync-skip-warning/index.test.ts new file mode 100644 index 000000000..e7c241e83 --- /dev/null +++ b/src/hooks/fsync-skip-warning/index.test.ts @@ -0,0 +1,98 @@ +import { beforeEach, describe, expect, it } from "bun:test" + +import { classifyPathEnvironment } from "../../shared/classify-path-environment" +import { clearAllSkips, recordFsyncSkip } from "../../shared/fsync-skip-tracker" +import { createFsyncSkipWarningHook } from "./index" + +describe("createFsyncSkipWarningHook", () => { + beforeEach(() => { + clearAllSkips() + }) + + it("records callID start timestamp in tool.execute.before", async () => { + const hook = createFsyncSkipWarningHook() + const input = { tool: "bash", sessionID: "ses1", callID: "call-1" } + const output = { args: {} as Record } + + await hook["tool.execute.before"](input, output) + await Bun.sleep(2) + + recordFsyncSkip({ + filePath: "/tmp/a", + contextLabel: "atomicWrite:/tmp/a", + errorCode: "EPERM", + message: "operation not permitted", + pathClassification: classifyPathEnvironment("/tmp/a"), + }) + + const afterOutput = { title: "ok", output: "done", metadata: {} as Record } + await hook["tool.execute.after"](input, afterOutput) + + expect(afterOutput.output).toContain("[fsync-skipped]") + }) + + it("drains skips after start time and appends warning to output text", async () => { + const hook = createFsyncSkipWarningHook() + const input = { tool: "write", sessionID: "ses1", callID: "call-2" } + const beforeOutput = { args: {} as Record } + const afterOutput = { title: "ok", output: "base", metadata: {} as Record } + + await hook["tool.execute.before"](input, beforeOutput) + await Bun.sleep(2) + + recordFsyncSkip({ + filePath: "/Users/x/OneDrive/a", + contextLabel: "atomicWrite:/Users/x/OneDrive/a", + errorCode: "EPERM", + message: "operation not permitted", + pathClassification: classifyPathEnvironment("/Users/x/OneDrive/a"), + }) + + await hook["tool.execute.after"](input, afterOutput) + + expect(afterOutput.output).toContain("base\n\n---") + expect(afterOutput.output).toContain("OneDrive") + }) + + it("leaves output unchanged when no skips happen during window", async () => { + const hook = createFsyncSkipWarningHook() + const input = { tool: "write", sessionID: "ses1", callID: "call-3" } + const beforeOutput = { args: {} as Record } + const afterOutput = { title: "ok", output: "base", metadata: {} as Record } + + await hook["tool.execute.before"](input, beforeOutput) + await hook["tool.execute.after"](input, afterOutput) + + expect(afterOutput.output).toBe("base") + }) + + it("isolates multiple parallel calls by callID watermark", async () => { + const hook = createFsyncSkipWarningHook() + const beforeOutput = { args: {} as Record } + + const inputA = { tool: "write", sessionID: "ses1", callID: "call-A" } + const inputB = { tool: "write", sessionID: "ses1", callID: "call-B" } + + await hook["tool.execute.before"](inputA, beforeOutput) + await Bun.sleep(2) + await hook["tool.execute.before"](inputB, beforeOutput) + await Bun.sleep(2) + + recordFsyncSkip({ + filePath: "/tmp/a", + contextLabel: "atomicWrite:/tmp/a", + errorCode: "EPERM", + message: "operation not permitted", + pathClassification: classifyPathEnvironment("/tmp/a"), + }) + + const outputA = { title: "ok", output: "A", metadata: {} as Record } + const outputB = { title: "ok", output: "B", metadata: {} as Record } + + await hook["tool.execute.after"](inputA, outputA) + await hook["tool.execute.after"](inputB, outputB) + + expect(outputA.output).toContain("[fsync-skipped]") + expect(outputB.output).toBe("B") + }) +}) diff --git a/src/hooks/fsync-skip-warning/index.ts b/src/hooks/fsync-skip-warning/index.ts new file mode 100644 index 000000000..fb59399be --- /dev/null +++ b/src/hooks/fsync-skip-warning/index.ts @@ -0,0 +1,50 @@ +import { drainSkipsAfter } from "../../shared/fsync-skip-tracker" +import { formatFsyncSkipWarning } from "../../shared/fsync-skip-warning-formatter" + +type ToolExecuteInput = { + tool: string + sessionID: string + callID: string +} + +type ToolBeforeOutput = { + args: Record +} + +type ToolAfterOutput = { + title: string + output: string + metadata: unknown +} + +export function createFsyncSkipWarningHook() { + const startTimesByCallId = new Map() + + const toolExecuteBefore = async ( + input: ToolExecuteInput, + _output: ToolBeforeOutput, + ): Promise => { + startTimesByCallId.set(input.callID, Date.now()) + } + + const toolExecuteAfter = async ( + input: ToolExecuteInput, + output: ToolAfterOutput, + ): Promise => { + if (typeof output.output !== "string") return + + const startTimestamp = startTimesByCallId.get(input.callID) ?? 0 + startTimesByCallId.delete(input.callID) + + const skips = drainSkipsAfter(startTimestamp) + const warning = formatFsyncSkipWarning(skips) + if (warning.length === 0) return + + output.output = `${output.output}\n\n${warning}` + } + + return { + "tool.execute.before": toolExecuteBefore, + "tool.execute.after": toolExecuteAfter, + } +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 98473c67a..5ed94b813 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -65,3 +65,4 @@ export { createReadImageResizerHook } from "./read-image-resizer" export { createTodoDescriptionOverrideHook } from "./todo-description-override" export { createWebFetchRedirectGuardHook } from "./webfetch-redirect-guard" export { createLegacyPluginToastHook } from "./legacy-plugin-toast" +export { createFsyncSkipWarningHook } from "./fsync-skip-warning" diff --git a/src/plugin/hooks/create-tool-guard-hooks.ts b/src/plugin/hooks/create-tool-guard-hooks.ts index 8f6675350..7cd8ea166 100644 --- a/src/plugin/hooks/create-tool-guard-hooks.ts +++ b/src/plugin/hooks/create-tool-guard-hooks.ts @@ -18,6 +18,7 @@ import { createTodoDescriptionOverrideHook, createWebFetchRedirectGuardHook, createTeamToolGating, + createFsyncSkipWarningHook, } from "../../hooks" import { getOpenCodeVersion, @@ -42,6 +43,7 @@ export type ToolGuardHooks = { readImageResizer: ReturnType | null todoDescriptionOverride: ReturnType | null webfetchRedirectGuard: ReturnType | null + fsyncSkipWarning: ReturnType | null teamToolGating: ReturnType | null } @@ -139,6 +141,10 @@ export function createToolGuardHooks(args: { ? safeHook("team-tool-gating", () => createTeamToolGating(ctx, pluginConfig.team_mode)) : null + const fsyncSkipWarning = isHookEnabled("fsync-skip-warning") + ? safeHook("fsync-skip-warning", () => createFsyncSkipWarningHook()) + : null + return { commentChecker, toolOutputTruncator, @@ -154,6 +160,7 @@ export function createToolGuardHooks(args: { readImageResizer, todoDescriptionOverride, webfetchRedirectGuard, + fsyncSkipWarning, teamToolGating, } } diff --git a/src/plugin/tool-execute-after.ts b/src/plugin/tool-execute-after.ts index f230d90db..7dabc7545 100644 --- a/src/plugin/tool-execute-after.ts +++ b/src/plugin/tool-execute-after.ts @@ -153,6 +153,7 @@ export function createToolExecuteAfterHandler(args: { await hooks.readImageResizer?.["tool.execute.after"]?.(hookInput, output) await hooks.hashlineReadEnhancer?.["tool.execute.after"]?.(hookInput, output) await hooks.webfetchRedirectGuard?.["tool.execute.after"]?.(hookInput, output) + await hooks.fsyncSkipWarning?.["tool.execute.after"]?.(hookInput, output) await hooks.jsonErrorRecovery?.["tool.execute.after"]?.(hookInput, output) } diff --git a/src/plugin/tool-execute-before.ts b/src/plugin/tool-execute-before.ts index e903571fe..093c3b157 100644 --- a/src/plugin/tool-execute-before.ts +++ b/src/plugin/tool-execute-before.ts @@ -72,8 +72,9 @@ export function createToolExecuteBeforeHandler(args: { await hooks.directoryReadmeInjector?.["tool.execute.before"]?.(input, output) await hooks.rulesInjector?.["tool.execute.before"]?.(input, output) await hooks.tasksTodowriteDisabler?.["tool.execute.before"]?.(input, output) - await hooks.webfetchRedirectGuard?.["tool.execute.before"]?.(input, output) - await hooks.prometheusMdOnly?.["tool.execute.before"]?.(input, output) + await hooks.webfetchRedirectGuard?.["tool.execute.before"]?.(input, output) + await hooks.fsyncSkipWarning?.["tool.execute.before"]?.(input, output) + await hooks.prometheusMdOnly?.["tool.execute.before"]?.(input, output) await hooks.sisyphusJuniorNotepad?.["tool.execute.before"]?.(input, output) await hooks.atlasHook?.["tool.execute.before"]?.(input, output) await hooks.teamToolGating?.["tool.execute.before"]?.(input, output) diff --git a/src/shared/fsync-skip-warning-formatter.test.ts b/src/shared/fsync-skip-warning-formatter.test.ts new file mode 100644 index 000000000..57b626e02 --- /dev/null +++ b/src/shared/fsync-skip-warning-formatter.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "bun:test" + +import type { FsyncSkipEntry } from "./fsync-skip-tracker" +import { formatFsyncSkipWarning } from "./fsync-skip-warning-formatter" + +function makeEntry(index: number, classification: FsyncSkipEntry["pathClassification"]): FsyncSkipEntry { + return { + filePath: `/path/${index}`, + contextLabel: `atomicWrite:/path/${index}`, + errorCode: "EPERM", + message: "operation not permitted", + pathClassification: classification, + timestamp: 1000 + index, + } +} + +describe("formatFsyncSkipWarning", () => { + it("returns empty string for zero entries", () => { + expect(formatFsyncSkipWarning([])).toBe("") + }) + + it("includes iCloud environment, path, and code for one entry", () => { + const warning = formatFsyncSkipWarning([makeEntry(1, "icloud")]) + expect(warning).toContain("iCloud Drive") + expect(warning).toContain("/path/1") + expect(warning).toContain("EPERM") + }) + + it("shows all five paths when exactly five entries exist", () => { + const warning = formatFsyncSkipWarning([ + makeEntry(1, "icloud"), + makeEntry(2, "icloud"), + makeEntry(3, "icloud"), + makeEntry(4, "icloud"), + makeEntry(5, "icloud"), + ]) + + expect(warning).toContain("/path/1") + expect(warning).toContain("/path/5") + expect(warning).not.toContain("and 1 more") + }) + + it("shows five paths plus overflow summary when six entries exist", () => { + const warning = formatFsyncSkipWarning([ + makeEntry(1, "icloud"), + makeEntry(2, "icloud"), + makeEntry(3, "icloud"), + makeEntry(4, "icloud"), + makeEntry(5, "icloud"), + makeEntry(6, "icloud"), + ]) + + expect(warning).toContain("/path/5") + expect(warning).not.toContain("/path/6") + expect(warning).toContain("... and 1 more") + }) + + it("uses the most common classification when entries are mixed", () => { + const warning = formatFsyncSkipWarning([ + makeEntry(1, "onedrive"), + makeEntry(2, "onedrive"), + makeEntry(3, "icloud"), + ]) + + expect(warning).toContain("Detected environment: OneDrive") + }) + + it("matches required section format", () => { + const warning = formatFsyncSkipWarning([makeEntry(1, "unknown")]) + + expect(warning).toContain("[fsync-skipped] 1 write(s) bypassed fsync") + expect(warning).toContain("Affected paths:") + expect(warning).toContain("What this means:") + expect(warning).toContain("The write+rename succeeded") + expect(warning).not.toContain("Detected environment:") + expect(warning).toContain("filesystem does not support fsync") + }) +}) diff --git a/src/shared/fsync-skip-warning-formatter.ts b/src/shared/fsync-skip-warning-formatter.ts new file mode 100644 index 000000000..91bd869d4 --- /dev/null +++ b/src/shared/fsync-skip-warning-formatter.ts @@ -0,0 +1,61 @@ +import { describePathClassification } from "./classify-path-environment" +import type { FsyncSkipEntry } from "./fsync-skip-tracker" + +const MAX_PATH_LINES = 5 + +function selectMostCommonClassification( + entries: FsyncSkipEntry[], +): FsyncSkipEntry["pathClassification"] { + const counts = new Map() + + for (const entry of entries) { + const currentCount = counts.get(entry.pathClassification) ?? 0 + counts.set(entry.pathClassification, currentCount + 1) + } + + let selected: FsyncSkipEntry["pathClassification"] = "unknown" + let selectedCount = -1 + for (const [classification, count] of counts.entries()) { + if (count > selectedCount) { + selected = classification + selectedCount = count + } + } + + return selected +} + +export function formatFsyncSkipWarning(entries: FsyncSkipEntry[]): string { + if (entries.length === 0) return "" + + const selectedClassification = selectMostCommonClassification(entries) + const selectedDescription = describePathClassification(selectedClassification) + const shownEntries = entries.slice(0, MAX_PATH_LINES) + const hiddenCount = Math.max(entries.length - shownEntries.length, 0) + const pathLines = shownEntries.map((entry) => ` - ${entry.filePath} (code: ${entry.errorCode})`) + if (hiddenCount > 0) { + pathLines.push(` ... and ${hiddenCount} more`) + } + + const environmentLines = selectedClassification === "unknown" + ? [] + : [`Detected environment: ${selectedDescription}`] + + const durabilityLine = selectedClassification === "unknown" + ? " - Crash durability is best-effort because this filesystem does not support fsync." + : " - Crash durability is best-effort on this filesystem (this is normal for iCloud, OneDrive, network drives, antivirus-locked paths)." + + return [ + "---", + `[fsync-skipped] ${entries.length} write(s) bypassed fsync because the underlying filesystem rejected the syscall.`, + "", + ...environmentLines, + "Affected paths:", + ...pathLines, + "", + "What this means:", + " - The write+rename succeeded — the file is on disk, atomicity is preserved.", + durabilityLine, + " - No action required. Operation completed successfully.", + ].join("\n") +} From e02166d6b71f40e73cbbf18d4ebe4bcad7124279 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 8 May 2026 15:25:51 +0900 Subject: [PATCH 8/8] fix(web): downgrade Next.js 16.2.6 -> 15.5.18 to unblock Cloudflare deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first auto-deploy from PR #3855 returned HTTP 500 on every page with the runtime error `TypeError: components.ComponentMod.handler is not a function`. Captured via `wrangler tail`. Root cause: Next.js 16.2.6 was published 2026-05-07 19:01 UTC, *after* @opennextjs/cloudflare 1.19.8 was published earlier the same day at 11:33 UTC. OpenNext 1.19.8's peerDependency declares `next: '>=15.5.16 <16 || >=16.2.5'` — 16.2.6 falls inside the range syntactically, but the route component module export shape changed in that patch and OpenNext has not caught up yet. Pin Next + eslint-config-next to 15.5.18 (latest 15.x LTS, the other half of OpenNext's supported range). Revert the migration-only changes that came with the 16 bump: - eslint.config.mjs: `nextPlugin.configs["core-web-vitals"]` (v16 shape) -> `nextPlugin.flatConfig.coreWebVitals` (v15 shape). - tsconfig.json: `jsx: "react-jsx"` -> `jsx: "preserve"` (Next 15 default). - tsconfig.json: add `noUncheckedSideEffectImports: false` because TypeScript 6 enabled this option under `strict` and Next 15's bundled types do not declare ambient CSS modules (Next 16 does). All other web/ deps stay at latest. lucide-react remains pinned at 0.577.0 from #3853 for the same brand-icon reason. Re-evaluate Next 16 when @opennextjs/cloudflare ships a release explicitly tested against \>= 16.2.6. --- web/bun.lock | 100 ++++++++---------------------------------- web/eslint.config.mjs | 2 +- web/package.json | 4 +- web/tsconfig.json | 3 +- 4 files changed, 23 insertions(+), 86 deletions(-) diff --git a/web/bun.lock b/web/bun.lock index 0c7721d18..46ad1eb68 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -13,7 +13,7 @@ "geist": "^1.7.0", "lucide-react": "0.577.0", "motion": "^12.38.0", - "next": "16.2.6", + "next": "15.5.18", "next-intl": "^4.11.0", "react": "^19.2.6", "react-dom": "^19.2.6", @@ -28,7 +28,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "eslint": "^10.3.0", - "eslint-config-next": "16.2.6", + "eslint-config-next": "15.5.18", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.5", "globals": "^17.6.0", @@ -163,38 +163,6 @@ "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="], - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - - "@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="], - - "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], - - "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], - - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], - - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - - "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], - - "@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], - - "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - - "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - - "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], @@ -375,25 +343,25 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], - "@next/env": ["@next/env@16.2.6", "", {}, "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw=="], + "@next/env": ["@next/env@15.5.18", "", {}, "sha512-hAV85Ckd9QR6RvH04MEKwsfLTksvFpO47j9xwtoIuvuPnlwecpSi+uZTtm8HirVbtlI2Fnz//xpcSTjFdyJk+g=="], - "@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.2.6", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw=="], + "@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.18", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-w4MYq8M26a8PNrfto0JosLf5/3ssln1rsyP96g2DkC8uFVymStM5DLSz5ElxxrPRg2XnTMnFo3kREFlhYvxhWw=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-w0WvQf1n+txiwns/9pwIQteCJpZTbxzO2SE0FLcwuD4v0WEh1JPOjdyxWL21XwJsdpx8cFRjyzxzCS/siP7HcQ=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-znn71QmDuxm+BOaglihMZfvyySMnNljkVIY5Z2TCssBmm+WqL6c19VhtH5ktFkHa8EZ2bnTUpcNcmNSQsg67og=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-yPPe5MNL+igZUa+OsqQJisqSfh6oarIuA1Q0BDxljGJhRQyZeP+WRHh7rs/jZUGMh5aY0YdIjXZG0VohkKkUdw=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-glaCczEWIrHsokFZ3pP08U4BpKxwIdnT+txdOM32OBgpL9Yw4aqx8NejmgtZQZOdstQ5f0L3CasIZudzCuD+nw=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.18", "", { "os": "linux", "cpu": "x64" }, "sha512-oUfg2EgJmU3R0OCOWiokGFUTvZiPfXtriXiuF3YNxRoROCdgvTedHIzYoeKH34gsZxS/V7mHbfq2hpAHwhH1/A=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.18", "", { "os": "linux", "cpu": "x64" }, "sha512-JLxSP3KTd9iu/bvUMQxH7RJo9xKSHf55/6RPE4a6FTSZygGn7uvZbCej0AHXydwkggQGSD9UddSjwv6Xz5ESfA=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-ir1v7enP52K2HNz3tQQvwF+x7VNxBk1ciiZ18WBPvxf4C59IqdfmHPJYK3vH7rSxpuCVw/8C712wTXNAtEp+NA=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.18", "", { "os": "win32", "cpu": "x64" }, "sha512-LIu5me6QTANCd25E7I5uIEfvgQ06RK7tvHAbYo3zCb3VpxQEPvMcSpd87NwUABDT6MbGPdEGR5VRiK4PPTJhQg=="], "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], @@ -489,6 +457,8 @@ "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.16.1", "", {}, "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag=="], + "@schummar/icu-type-parser": ["@schummar/icu-type-parser@1.21.5", "", {}, "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw=="], "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], @@ -787,8 +757,6 @@ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.27", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA=="], - "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], @@ -799,8 +767,6 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -811,7 +777,7 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "caniuse-lite": ["caniuse-lite@1.0.30001769", "", {}, "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -839,8 +805,6 @@ "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], @@ -885,8 +849,6 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.352", "", {}, "sha512-9wHk8x6dyuimoe18EdiDPWKExNdxYqo4fn4FwOVVper6RxT3cmpBwBkWWfSOCYJjQdIco/nPhJhNLmn4Ufg1Yg=="], - "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], @@ -923,7 +885,7 @@ "eslint": ["eslint@10.3.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw=="], - "eslint-config-next": ["eslint-config-next@16.2.6", "", { "dependencies": { "@next/eslint-plugin-next": "16.2.6", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q=="], + "eslint-config-next": ["eslint-config-next@15.5.18", "", { "dependencies": { "@next/eslint-plugin-next": "15.5.18", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-HuoJU6uUPD00eyiud78IBnT4HLhztFj2V+ild2Uon5ZUrYZKe0Olu2QRD99e9IgL4/H1eg5Onka3BsfRW2U0Xw=="], "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], @@ -941,7 +903,7 @@ "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], - "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="], + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], @@ -1025,8 +987,6 @@ "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], @@ -1067,10 +1027,6 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], - - "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], - "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], @@ -1161,8 +1117,6 @@ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], @@ -1263,7 +1217,7 @@ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "next": ["next@16.2.6", "", { "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-x64": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-musl": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw=="], + "next": ["next@15.5.18", "", { "dependencies": { "@next/env": "15.5.18", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.18", "@next/swc-darwin-x64": "15.5.18", "@next/swc-linux-arm64-gnu": "15.5.18", "@next/swc-linux-arm64-musl": "15.5.18", "@next/swc-linux-x64-gnu": "15.5.18", "@next/swc-linux-x64-musl": "15.5.18", "@next/swc-win32-arm64-msvc": "15.5.18", "@next/swc-win32-x64-msvc": "15.5.18", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-eKL8zUJkX9Y5lE+RX/2YJoItVdGlIscyVyboeD9wSpp0PaGqjoA4tTpT2qPqz9ax+5IzGESyLSeZ/RCwbSZ2uQ=="], "next-intl": ["next-intl@4.11.0", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.8.1", "@parcel/watcher": "^2.4.1", "@swc/core": "^1.15.2", "icu-minify": "^4.11.0", "negotiator": "^1.0.0", "next-intl-swc-plugin-extractor": "^4.11.0", "po-parser": "^2.1.1", "use-intl": "^4.11.0" }, "peerDependencies": { "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" } }, "sha512-Chp8rgEVUYOX/bCtYy+PXH6lDX3X+GPT9sR9HScHroL283em/4urP9btfdHEMEHJJXdq2W/5wDaDDtWONPdNSA=="], @@ -1275,8 +1229,6 @@ "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], - "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -1521,8 +1473,6 @@ "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="], - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="], @@ -1561,8 +1511,6 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], "yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], @@ -1575,10 +1523,6 @@ "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], - "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - - "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], - "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], @@ -1591,10 +1535,6 @@ "@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.982.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ=="], - "@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -1627,16 +1567,12 @@ "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "browserslist/caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="], - "cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], "cloudflare/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "eslint-config-next/globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], - "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index 4227a6de6..380136378 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -18,7 +18,7 @@ export default [ }, }, }, - nextPlugin.configs["core-web-vitals"], + nextPlugin.flatConfig.coreWebVitals, ...tseslint.configs.recommended, prettier, { diff --git a/web/package.json b/web/package.json index 1463a7f6f..5662213bc 100644 --- a/web/package.json +++ b/web/package.json @@ -28,7 +28,7 @@ "geist": "^1.7.0", "lucide-react": "0.577.0", "motion": "^12.38.0", - "next": "16.2.6", + "next": "15.5.18", "next-intl": "^4.11.0", "react": "^19.2.6", "react-dom": "^19.2.6", @@ -43,7 +43,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "eslint": "^10.3.0", - "eslint-config-next": "16.2.6", + "eslint-config-next": "15.5.18", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.5", "globals": "^17.6.0", diff --git a/web/tsconfig.json b/web/tsconfig.json index c74d523e0..99f4f6f89 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -11,13 +11,14 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "noUncheckedIndexedAccess": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, + "noUncheckedSideEffectImports": false, "forceConsistentCasingInFileNames": true, "plugins": [ {