mirror of
https://github.com/saltbo/zpan.git
synced 2026-10-30 18:31:36 +08:00
* fix(downloads): block SSRF targets in remote-download source URL The remote-download source URI was only length-validated, so an authenticated editor could point a task at the cloud metadata endpoint, loopback, or RFC 1918 hosts and have the response exfiltrated to their own drive. Add a shared isSafeHttpUrl/isBlockedUrlHost guard (scheme allowlist + private/loopback/link-local/metadata/IPv6 blocking) and cross-check source type vs uri in createDownloadTaskSchema. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(api): cover 9 untested src/lib/api.ts wrappers Adds api.test.ts coverage (RPC path, method, payload, success + ApiError paths) for listObjectsByPath, isNameConflictError, listAdminAuthProviders, upsertAuthProvider, deleteAuthProvider, listInviteCodes, generateInviteCodes, deleteInviteCode, and listTeamActivities — satisfying the CLAUDE.md coverage gate that otherwise blocks PRs touching api.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(spaces): release source quota on cross-space move A cross-space "move" copied bytes into the target (reserving quota there) but only trashed the source. Trashed files still count toward usage, so the moved bytes were billed in both spaces and the source never freed — contrary to the design doc ("copy + delete source, quota effectively transfers"). Purge the source subtree (independent S3 copy already exists in the target) instead of trashing it, which deletes the objects, cascades share cleanup, and reconciles usage. Rename the response field sourceTrashed -> sourceDeleted and update the move hint copy accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(upload): wire S3 multipart for large files The upload UI only ever did a single presigned PUT, which caps at S3's 5 GiB limit and fails the whole transfer on any network blip — despite a complete multipart backend (object-upload-sessions) sitting unused. Add uploadPartToS3 (PUTs a part, returns its ETag) and a multipart-upload orchestrator: open session -> presign parts in batches of 100 -> PUT parts with bounded concurrency and per-part retry -> complete. Files over 100 MiB take this path; smaller files keep the single-PUT flow. Cancellation aborts the multipart and the draft. Also fixes the presignObjectUploadParts wrapper type to match the server's actual `url` field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(auth): add password-reset flow There was no self-service password recovery — a forgotten password needed admin intervention. SMTP/email sending was already built; this wires the last mile: better-auth sendResetPassword (reset email), a "Forgot password?" link on sign-in, and /forgot-password + /reset-password pages. The forgot-password page never reveals whether an account exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(trash): auto-purge trashed items past a retention window Trashed files counted toward quota forever — trash never auto-emptied, so storage was never reclaimed without a manual "empty trash". Add a daily cron (CF Workers 0 4 * * * + Node setInterval) that purges trashed items older than ZPAN_TRASH_RETENTION_DAYS (default 30, 0 disables) across all orgs, reusing the existing purge path so S3 objects, share references, and quota are all cleaned. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(notifications): typed NotificationType, i18n rendering, team-join Notifications were a bare-string type with only 3 producers, and server copy was stored as hardcoded English (zh users saw English). - Add a NotificationType union in shared/ and type the notification service. - Render notification title/body client-side from type + metadata via i18n, falling back to stored strings for older rows (fixes the hardcoded-English gap). - Notify users when they join a team (team_join). (Login auditing was intentionally dropped: reusing the activity-events feed for sign_in events would spam every user's per-org activity timeline. Proper auth auditing belongs in a dedicated log and can be added separately.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: cover SSRF guard and multipart upload branches Raise patch coverage on the new code: uploadPartToS3 pre-aborted-signal and network-error paths, the url-safety octet-overflow and public-IPv6 branches, and the invalid-magnet rejection in the download-task schema. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(storage): type S3Service against a narrow credentials shape The hand-written shared Storage type had a phantom `uid` field and lacked `filePath`, diverging from the DB row, so 44 call sites bridged the gap with `as unknown as S3Storage`. Introduce S3StorageCredentials (the 6 fields the S3 client actually reads); DB storage rows satisfy it structurally, so all casts are gone. Fix the shared Storage type to match the real API response (drop uid, add filePath, nullable customHost). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(storage): dedupe fileExt into path-template fileExt() was defined byte-identically in objects.ts, webdav.ts, and save-to-drive.ts, all feeding buildObjectKey. Move it next to buildObjectKey in path-template.ts and import it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(deps): remove 10 unused packages and the trash format-utils shim After migrating to the unified radix-ui package the individual @radix-ui/react-* packages (avatar, dialog, dropdown-menu, label, separator, slot, tooltip) were orphaned, along with @dnd-kit/sortable, @dnd-kit/utilities (only @dnd-kit/core is used), and @opentelemetry/api (transitive via better-auth, not imported directly). Also delete src/components/trash/format-utils.ts — a pure re-export of @/lib/format whose only consumer was its own test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(ui): dedupe getInitials into @/lib/format getInitials was reimplemented in 6 components/routes (user menu, org switcher, share layout, profile, users list, team settings) — behaviorally identical to the canonical @/lib/format.getInitials already used by the admin pages. Replace all locals with the shared import. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(traffic): extract consumeAndReportDownloadTraffic The consume-quota (422) -> report-egress (402, refund) preamble was hand-rolled in the object, landing-share, direct-share, and WebDAV download paths. Extract consumeAndReportDownloadTraffic, parameterizing the 422 renderer (JSON vs text) and the compensating action (share download-counter decrement). The image-host redirect path reports after presigning, so it keeps its own sequence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
176 lines
7.0 KiB
JSON
176 lines
7.0 KiB
JSON
{
|
|
"name": "zpan",
|
|
"version": "2.7.4",
|
|
"private": true,
|
|
"license": "AGPL-3.0-only",
|
|
"type": "module",
|
|
"packageManager": "pnpm@10.30.2",
|
|
"scripts": {
|
|
"dev": "CLOUDFLARE_ENV=staging vite dev",
|
|
"dev:node": "node --env-file=.dev.vars node_modules/vite/bin/vite.js dev --mode node",
|
|
"build": "[ \"$WORKERS_CI\" = \"1\" ] && [ \"$WORKERS_CI_BRANCH\" != \"main\" ] && export CLOUDFLARE_ENV=staging; vite build",
|
|
"build:node": "vite build --mode node && tsup server/entry-node.ts --config server/tsup.config.mjs --format esm --outDir dist-server --external better-sqlite3 --external @libsql/client",
|
|
"build:lambda": "tsup server/entry-lambda.ts --config server/tsup.config.mjs --format cjs --outDir dist-lambda --external @libsql/client",
|
|
"build:vercel": "vite build --mode node && tsup server/entry-vercel.ts --config server/tsup.config.mjs --format esm --outDir api --external @libsql/client",
|
|
"build:netlify": "tsup server/entry-netlify.ts --config server/tsup.config.mjs --format esm --outDir netlify/functions --external @libsql/client",
|
|
"build:azure": "vite build && tsup server/entry-azure.ts --config server/tsup.config.mjs --format esm --outDir azure-functions --external @azure/functions --external @libsql/client && cp server/azure-host.json azure-functions/host.json && cp -r dist azure-functions/dist",
|
|
"deploy": "pnpm db:migrate:d1:prod && wrangler deploy",
|
|
"release": "node scripts/release.mjs",
|
|
"release:check-version": "node scripts/check-release-version.mjs",
|
|
"db:generate": "drizzle-kit generate",
|
|
"db:migrate": "drizzle-kit migrate",
|
|
"db:migrate:d1": "wrangler d1 migrations apply zpan-db-staging --local --env staging",
|
|
"db:migrate:d1:staging": "wrangler d1 migrations apply zpan-db-staging --remote --env staging",
|
|
"db:migrate:d1:prod": "wrangler d1 migrations apply zpan-db --remote",
|
|
"db:reset": "tsx --env-file=.dev.vars scripts/db-reset.ts",
|
|
"db:reset:d1": "tsx --env-file=.dev.vars scripts/db-reset.ts --d1",
|
|
"typecheck": "tsc --noEmit -p server/tsconfig.json && tsc --noEmit -p src/tsconfig.json",
|
|
"test": "vitest run --project unit --project integration",
|
|
"test:cf": "vitest run --project cloudflare",
|
|
"test:libsql": "vitest run --project libsql",
|
|
"test:watch": "vitest --project unit --project integration",
|
|
"lint": "biome check .",
|
|
"lint:fix": "biome check --write .",
|
|
"prepare": "husky",
|
|
"format": "biome format --write .",
|
|
"e2e": "playwright test",
|
|
"e2e:cloud": "node scripts/run-cloud-e2e.mjs",
|
|
"e2e:cloud:cf": "node scripts/run-cloud-e2e.mjs --runtime cf",
|
|
"e2e:archive": "node scripts/run-cloud-e2e.mjs --local --with-s3-mock --spec archive.spec.ts",
|
|
"e2e:archive:cf": "node scripts/run-cloud-e2e.mjs --runtime cf --local --with-s3-mock --spec archive.spec.ts",
|
|
"openapi:downloader": "tsx scripts/generate-downloader-openapi.ts",
|
|
"openapi:downloader:go": "pnpm openapi:downloader && cd cmd && go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.7.0 -config oapi-codegen.yaml ../docs/openapi/downloader.json",
|
|
"openapi:downloader:check": "tsx scripts/check-downloader-openapi.ts",
|
|
"openapi:downloader:all": "pnpm openapi:downloader:go"
|
|
},
|
|
"engines": {
|
|
"node": ">=24"
|
|
},
|
|
"dependencies": {
|
|
"@aws-sdk/client-s3": "^3.1022.0",
|
|
"@aws-sdk/s3-request-presigner": "^3.1022.0",
|
|
"@azure/functions": "^4.12.0",
|
|
"@better-auth/api-key": "^1.6.14",
|
|
"@better-captcha/react": "^0.7.0",
|
|
"@dnd-kit/core": "^6.3.1",
|
|
"@hono/node-server": "^1.13.0",
|
|
"@hono/zod-openapi": "^1.4.0",
|
|
"@hono/zod-validator": "^0.7.6",
|
|
"@hookform/resolvers": "^5.2.2",
|
|
"@libsql/client": "^0.17.2",
|
|
"@tanstack/react-query": "^5.100.9",
|
|
"@tanstack/react-router": "^1.93.0",
|
|
"@tanstack/react-table": "^8.21.3",
|
|
"@uiw/react-markdown-preview": "5.2.0",
|
|
"@uiw/react-md-editor": "^4.1.0",
|
|
"@vidstack/react": "^1.12.13",
|
|
"better-auth": "^1.6.14",
|
|
"better-sqlite3": "^12.10.0",
|
|
"class-variance-authority": "^0.7.1",
|
|
"clsx": "^2.1.1",
|
|
"drizzle-orm": "^0.45.2",
|
|
"fflate": "^0.8.2",
|
|
"hono": "^4.12.21",
|
|
"i18next": "^26.0.3",
|
|
"i18next-browser-languagedetector": "^8.2.1",
|
|
"lucide-react": "^0.577.0",
|
|
"nanoid": "^5.1.11",
|
|
"next-themes": "^0.4.6",
|
|
"nodemailer": "^8.0.5",
|
|
"paseto-ts": "^2.0.6",
|
|
"pdfjs-dist": "^5.6.205",
|
|
"posthog-node": "^5.36.4",
|
|
"radix-ui": "^1.4.3",
|
|
"react": "^19.2.5",
|
|
"react-dom": "^19.2.5",
|
|
"react-dropzone": "^15.0.0",
|
|
"react-hook-form": "^7.54.0",
|
|
"react-i18next": "^17.0.2",
|
|
"react-markdown": "^10.1.0",
|
|
"react-pdf": "^10.4.1",
|
|
"remark-gfm": "^4.0.1",
|
|
"shiki": "^4.0.2",
|
|
"simple-icons": "^16.18.0",
|
|
"sonner": "^2.0.7",
|
|
"tailwind-merge": "^3.5.0",
|
|
"yet-another-react-lightbox": "^3.30.1",
|
|
"zod": "^4.4.3",
|
|
"zpan-cloud-sdk": "^2.2.0"
|
|
},
|
|
"devDependencies": {
|
|
"@biomejs/biome": "^2.4.10",
|
|
"@cloudflare/vite-plugin": "^1.39.0",
|
|
"@cloudflare/vitest-pool-workers": "^0.16.10",
|
|
"@cloudflare/workers-types": "^4.20260606.1",
|
|
"@hono/vite-dev-server": "^0.25.1",
|
|
"@playwright/test": "^1.59.1",
|
|
"@tailwindcss/vite": "^4.2.2",
|
|
"@tanstack/router-plugin": "^1.167.12",
|
|
"@testing-library/dom": "^10.4.1",
|
|
"@testing-library/react": "^16.3.2",
|
|
"@testing-library/user-event": "^14.6.1",
|
|
"@types/better-sqlite3": "^7.6.0",
|
|
"@types/nodemailer": "^8.0.0",
|
|
"@types/react": "^19.2.14",
|
|
"@types/react-dom": "^19.2.3",
|
|
"@vitejs/plugin-react-swc": "^4.3.0",
|
|
"@vitest/coverage-v8": "^4.1.2",
|
|
"drizzle-kit": "^0.31.10",
|
|
"husky": "^9.1.7",
|
|
"jsdom": "^29.0.2",
|
|
"lint-staged": "^16.4.0",
|
|
"tailwindcss": "^4.2.4",
|
|
"tsup": "^8.5.1",
|
|
"tsx": "^4.0.0",
|
|
"tw-animate-css": "^1.4.0",
|
|
"typescript": "^5.9.3",
|
|
"vite": "^7.3.2",
|
|
"vitest": "^4.1.2",
|
|
"webdav": "^5.10.0",
|
|
"wrangler": "^4.95.0"
|
|
},
|
|
"cloudflare": {
|
|
"bindings": {
|
|
"BETTER_AUTH_SECRET": {
|
|
"description": "Signing secret for auth sessions. Generate with: `openssl rand -base64 32`"
|
|
},
|
|
"DB": {
|
|
"description": "D1 database for storing users, files, and settings"
|
|
}
|
|
}
|
|
},
|
|
"lint-staged": {
|
|
"*.{ts,tsx,js,jsx,json,jsonc}": "biome check --write --no-errors-on-unmatched"
|
|
},
|
|
"pnpm": {
|
|
"overrides": {
|
|
"@better-auth/core": "1.6.14",
|
|
"@better-auth/drizzle-adapter": "1.6.14",
|
|
"@better-auth/kysely-adapter": "1.6.14",
|
|
"@better-auth/memory-adapter": "1.6.14",
|
|
"@better-auth/mongo-adapter": "1.6.14",
|
|
"@better-auth/prisma-adapter": "1.6.14",
|
|
"@better-auth/telemetry": "1.6.14",
|
|
"@vitest/expect": "4.1.4",
|
|
"@vitest/mocker": "4.1.4",
|
|
"@vitest/pretty-format": "4.1.4",
|
|
"@vitest/runner": "4.1.4",
|
|
"@vitest/snapshot": "4.1.4",
|
|
"@vitest/spy": "4.1.4",
|
|
"@vitest/utils": "4.1.4",
|
|
"undici": "7.24.8"
|
|
},
|
|
"onlyBuiltDependencies": [
|
|
"@swc/core",
|
|
"better-sqlite3",
|
|
"esbuild",
|
|
"sharp",
|
|
"workerd"
|
|
]
|
|
},
|
|
"volta": {
|
|
"node": "24.14.1",
|
|
"pnpm": "10.30.2"
|
|
}
|
|
}
|