Showing preview only (9,478K chars total). Download the full file or copy to clipboard to get everything.
Repository: ridafkih/keeper.sh
Branch: main
Commit: 52256a7abf93
Files: 913
Total size: 8.8 MB
Directory structure:
gitextract_47nb8t9x/
├── .claude/
│ ├── settings.json
│ └── skills/
│ └── react-best-practices/
│ ├── AGENTS.md
│ ├── README.md
│ ├── SKILL.md
│ ├── metadata.json
│ └── rules/
│ ├── _sections.md
│ ├── _template.md
│ ├── advanced-event-handler-refs.md
│ ├── advanced-use-latest.md
│ ├── async-api-routes.md
│ ├── async-defer-await.md
│ ├── async-dependencies.md
│ ├── async-parallel.md
│ ├── async-suspense-boundaries.md
│ ├── bundle-barrel-imports.md
│ ├── bundle-conditional.md
│ ├── bundle-defer-third-party.md
│ ├── bundle-dynamic-imports.md
│ ├── bundle-preload.md
│ ├── client-event-listeners.md
│ ├── client-swr-dedup.md
│ ├── js-batch-dom-css.md
│ ├── js-cache-function-results.md
│ ├── js-cache-property-access.md
│ ├── js-cache-storage.md
│ ├── js-combine-iterations.md
│ ├── js-early-exit.md
│ ├── js-hoist-regexp.md
│ ├── js-index-maps.md
│ ├── js-length-check-first.md
│ ├── js-min-max-loop.md
│ ├── js-set-map-lookups.md
│ ├── js-tosorted-immutable.md
│ ├── rendering-activity.md
│ ├── rendering-animate-svg-wrapper.md
│ ├── rendering-conditional-render.md
│ ├── rendering-content-visibility.md
│ ├── rendering-hoist-jsx.md
│ ├── rendering-hydration-no-flicker.md
│ ├── rendering-svg-precision.md
│ ├── rerender-defer-reads.md
│ ├── rerender-dependencies.md
│ ├── rerender-derived-state.md
│ ├── rerender-functional-setstate.md
│ ├── rerender-lazy-state-init.md
│ ├── rerender-memo.md
│ ├── rerender-transitions.md
│ ├── server-after-nonblocking.md
│ ├── server-cache-lru.md
│ ├── server-cache-react.md
│ ├── server-parallel-fetching.md
│ └── server-serialization.md
├── .dockerignore
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ ├── checks.yml
│ └── docker-publish.yml
├── .gitignore
├── .oxlintrc.json
├── Caddyfile
├── LICENSE
├── NOTICE
├── README.md
├── applications/
│ └── web/
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── Dockerfile
│ ├── README.md
│ ├── entrypoint.sh
│ ├── eslint.config.js
│ ├── index.html
│ ├── package.json
│ ├── plugins/
│ │ ├── blog.ts
│ │ └── sitemap.ts
│ ├── public/
│ │ ├── llms-full.txt
│ │ ├── llms.txt
│ │ ├── robots.txt
│ │ └── site.webmanifest
│ ├── scripts/
│ │ ├── build.ts
│ │ └── start.ts
│ ├── src/
│ │ ├── components/
│ │ │ ├── analytics-scripts.tsx
│ │ │ ├── cookie-consent.tsx
│ │ │ └── ui/
│ │ │ ├── composites/
│ │ │ │ └── navigation-menu/
│ │ │ │ ├── navigation-menu-editable.tsx
│ │ │ │ ├── navigation-menu-items.tsx
│ │ │ │ ├── navigation-menu-popover.tsx
│ │ │ │ ├── navigation-menu.contexts.ts
│ │ │ │ └── navigation-menu.styles.ts
│ │ │ ├── primitives/
│ │ │ │ ├── animated-reveal.tsx
│ │ │ │ ├── back-button.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── checkbox.tsx
│ │ │ │ ├── collapsible.tsx
│ │ │ │ ├── dashboard-heading.tsx
│ │ │ │ ├── delete-confirmation.tsx
│ │ │ │ ├── divider.tsx
│ │ │ │ ├── error-state.tsx
│ │ │ │ ├── fade-in.tsx
│ │ │ │ ├── github-star-button.tsx
│ │ │ │ ├── heading.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── list.tsx
│ │ │ │ ├── markdown-component-map.ts
│ │ │ │ ├── markdown-components.tsx
│ │ │ │ ├── modal.tsx
│ │ │ │ ├── pagination.tsx
│ │ │ │ ├── provider-icon-stack.tsx
│ │ │ │ ├── provider-icon.tsx
│ │ │ │ ├── shimmer-text.tsx
│ │ │ │ ├── staggered-backdrop-blur.tsx
│ │ │ │ ├── template-text.tsx
│ │ │ │ ├── text-link.tsx
│ │ │ │ ├── text.tsx
│ │ │ │ ├── tooltip.tsx
│ │ │ │ └── upgrade-hint.tsx
│ │ │ └── shells/
│ │ │ ├── layout.tsx
│ │ │ ├── route-shell.tsx
│ │ │ └── session-slot.tsx
│ │ ├── config/
│ │ │ ├── commercial.ts
│ │ │ ├── gdpr.ts
│ │ │ └── plans.ts
│ │ ├── content/
│ │ │ └── blog/
│ │ │ └── introducing-keeper-blog.mdx
│ │ ├── features/
│ │ │ ├── auth/
│ │ │ │ └── components/
│ │ │ │ ├── auth-form.tsx
│ │ │ │ ├── auth-switch-prompt.tsx
│ │ │ │ ├── caldav-connect-form.tsx
│ │ │ │ ├── caldav-connect-page.tsx
│ │ │ │ ├── ics-connect-form.tsx
│ │ │ │ └── oauth-preamble.tsx
│ │ │ ├── blog/
│ │ │ │ └── components/
│ │ │ │ └── blog-post-cta.tsx
│ │ │ ├── dashboard/
│ │ │ │ └── components/
│ │ │ │ ├── event-graph.tsx
│ │ │ │ ├── metadata-row.tsx
│ │ │ │ ├── sync-status-helpers.ts
│ │ │ │ ├── sync-status.tsx
│ │ │ │ └── upgrade-card.tsx
│ │ │ └── marketing/
│ │ │ ├── components/
│ │ │ │ ├── marketing-cta.tsx
│ │ │ │ ├── marketing-faq.tsx
│ │ │ │ ├── marketing-feature-bento.tsx
│ │ │ │ ├── marketing-footer.tsx
│ │ │ │ ├── marketing-header.tsx
│ │ │ │ ├── marketing-how-it-works.tsx
│ │ │ │ ├── marketing-illustration-calendar.tsx
│ │ │ │ └── marketing-pricing-section.tsx
│ │ │ └── contributors.json
│ │ ├── generated/
│ │ │ └── tanstack/
│ │ │ └── route-tree.generated.ts
│ │ ├── hooks/
│ │ │ ├── use-animated-swr.ts
│ │ │ ├── use-api-tokens.ts
│ │ │ ├── use-entitlements.ts
│ │ │ ├── use-events.ts
│ │ │ ├── use-has-password.ts
│ │ │ ├── use-passkeys.ts
│ │ │ ├── use-session.ts
│ │ │ ├── use-start-of-today.ts
│ │ │ └── use-subscription.ts
│ │ ├── illustrations/
│ │ │ ├── how-it-works-configure.tsx
│ │ │ ├── how-it-works-connect.tsx
│ │ │ ├── how-it-works-sync.tsx
│ │ │ ├── marketing-illustration-contributors.tsx
│ │ │ ├── marketing-illustration-providers.tsx
│ │ │ ├── marketing-illustration-setup.tsx
│ │ │ └── marketing-illustration-sync.tsx
│ │ ├── index.css
│ │ ├── index.d.ts
│ │ ├── lib/
│ │ │ ├── analytics.ts
│ │ │ ├── auth-capabilities.ts
│ │ │ ├── auth-client.ts
│ │ │ ├── auth.ts
│ │ │ ├── blog-posts.ts
│ │ │ ├── fetcher.ts
│ │ │ ├── mcp-auth-flow.ts
│ │ │ ├── motion-features.ts
│ │ │ ├── page-metadata.ts
│ │ │ ├── pluralize.ts
│ │ │ ├── providers.ts
│ │ │ ├── route-access-guards.ts
│ │ │ ├── router-context.ts
│ │ │ ├── runtime-config.ts
│ │ │ ├── seo.ts
│ │ │ ├── serialized-mutate.ts
│ │ │ ├── session-cookie.ts
│ │ │ ├── swr.ts
│ │ │ └── time.ts
│ │ ├── main.tsx
│ │ ├── providers/
│ │ │ ├── sync-provider-logic.ts
│ │ │ └── sync-provider.tsx
│ │ ├── routeTree.gen.ts
│ │ ├── router.ts
│ │ ├── routes/
│ │ │ ├── (auth)/
│ │ │ │ ├── forgot-password.tsx
│ │ │ │ ├── login.tsx
│ │ │ │ ├── register.tsx
│ │ │ │ ├── reset-password.tsx
│ │ │ │ ├── route.tsx
│ │ │ │ ├── verify-authentication.tsx
│ │ │ │ └── verify-email.tsx
│ │ │ ├── (dashboard)/
│ │ │ │ ├── dashboard/
│ │ │ │ │ ├── accounts/
│ │ │ │ │ │ ├── $accountId.$calendarId.tsx
│ │ │ │ │ │ ├── $accountId.index.tsx
│ │ │ │ │ │ ├── $accountId.setup.tsx
│ │ │ │ │ │ └── route.tsx
│ │ │ │ │ ├── connect/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── route.tsx
│ │ │ │ │ ├── events/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── feedback.tsx
│ │ │ │ │ ├── ical.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── integrations/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── report.tsx
│ │ │ │ │ ├── settings/
│ │ │ │ │ │ ├── api-tokens.tsx
│ │ │ │ │ │ ├── change-password.tsx
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── passkeys.tsx
│ │ │ │ │ │ └── route.tsx
│ │ │ │ │ └── upgrade/
│ │ │ │ │ └── index.tsx
│ │ │ │ └── route.tsx
│ │ │ ├── (marketing)/
│ │ │ │ ├── blog/
│ │ │ │ │ ├── $slug.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── route.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ ├── privacy.tsx
│ │ │ │ ├── route.tsx
│ │ │ │ └── terms.tsx
│ │ │ ├── (oauth)/
│ │ │ │ ├── auth/
│ │ │ │ │ ├── google.tsx
│ │ │ │ │ ├── outlook.tsx
│ │ │ │ │ └── route.tsx
│ │ │ │ ├── dashboard/
│ │ │ │ │ ├── connect/
│ │ │ │ │ │ ├── apple.tsx
│ │ │ │ │ │ ├── caldav.tsx
│ │ │ │ │ │ ├── fastmail.tsx
│ │ │ │ │ │ ├── google.tsx
│ │ │ │ │ │ ├── ical-link.tsx
│ │ │ │ │ │ ├── ics-file.tsx
│ │ │ │ │ │ ├── microsoft.tsx
│ │ │ │ │ │ ├── outlook.tsx
│ │ │ │ │ │ └── route.tsx
│ │ │ │ │ └── route.tsx
│ │ │ │ ├── oauth/
│ │ │ │ │ └── consent.tsx
│ │ │ │ └── route.tsx
│ │ │ └── __root.tsx
│ │ ├── server/
│ │ │ ├── cache/
│ │ │ │ └── stale-cache.ts
│ │ │ ├── compression.ts
│ │ │ ├── config.ts
│ │ │ ├── github-stars.ts
│ │ │ ├── http-handler.ts
│ │ │ ├── index.ts
│ │ │ ├── internal-routes.ts
│ │ │ ├── logging.ts
│ │ │ ├── migration-check.ts
│ │ │ ├── paths.ts
│ │ │ ├── proxy/
│ │ │ │ ├── http.ts
│ │ │ │ └── websocket.ts
│ │ │ ├── runtime.ts
│ │ │ ├── types.ts
│ │ │ └── vite-assets.ts
│ │ ├── server.tsx
│ │ ├── state/
│ │ │ ├── auth-form.ts
│ │ │ ├── calendar-detail.ts
│ │ │ ├── calendar-emphasized.ts
│ │ │ ├── destination-ids.ts
│ │ │ ├── event-graph-hover.ts
│ │ │ ├── ical-feed-settings.ts
│ │ │ ├── ical-sources.ts
│ │ │ ├── popover-overlay.ts
│ │ │ └── sync.ts
│ │ ├── types/
│ │ │ └── api.ts
│ │ ├── utils/
│ │ │ ├── calendars.ts
│ │ │ ├── checkout.ts
│ │ │ ├── cn.ts
│ │ │ ├── collections.ts
│ │ │ ├── errors.ts
│ │ │ └── templates.ts
│ │ └── vite-env.d.ts
│ ├── tests/
│ │ ├── features/
│ │ │ ├── auth/
│ │ │ │ └── components/
│ │ │ │ └── auth-form.test.tsx
│ │ │ └── dashboard/
│ │ │ └── components/
│ │ │ └── sync-status-helpers.test.ts
│ │ ├── hooks/
│ │ │ └── use-entitlements.test.ts
│ │ ├── lib/
│ │ │ ├── auth-capabilities.test.ts
│ │ │ ├── auth.test.ts
│ │ │ ├── mcp-auth-flow.test.ts
│ │ │ ├── route-access-guards.test.ts
│ │ │ ├── runtime-config.test.ts
│ │ │ └── serialized-mutate.test.ts
│ │ ├── providers/
│ │ │ └── sync-provider-logic.test.ts
│ │ ├── server/
│ │ │ └── internal-routes.test.ts
│ │ └── state/
│ │ └── sync.test.ts
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── vite.config.ts
│ └── vitest.config.ts
├── bunfig.toml
├── compose.yaml
├── deploy/
│ ├── Caddyfile
│ └── compose.yaml
├── docker/
│ ├── caddy/
│ │ └── Dockerfile
│ ├── services/
│ │ ├── Dockerfile
│ │ └── rootfs/
│ │ └── etc/
│ │ └── s6-overlay/
│ │ ├── s6-rc.d/
│ │ │ ├── api/
│ │ │ │ ├── dependencies.d/
│ │ │ │ │ └── init-db
│ │ │ │ ├── run
│ │ │ │ └── type
│ │ │ ├── cron/
│ │ │ │ ├── dependencies.d/
│ │ │ │ │ └── init-db
│ │ │ │ ├── run
│ │ │ │ └── type
│ │ │ ├── init-db/
│ │ │ │ ├── type
│ │ │ │ └── up
│ │ │ ├── user/
│ │ │ │ ├── contents.d/
│ │ │ │ │ ├── api
│ │ │ │ │ ├── cron
│ │ │ │ │ ├── init-db
│ │ │ │ │ ├── web
│ │ │ │ │ └── worker
│ │ │ │ └── type
│ │ │ ├── web/
│ │ │ │ ├── dependencies.d/
│ │ │ │ │ └── api
│ │ │ │ ├── run
│ │ │ │ └── type
│ │ │ └── worker/
│ │ │ ├── dependencies.d/
│ │ │ │ └── init-db
│ │ │ ├── run
│ │ │ └── type
│ │ └── scripts/
│ │ └── init-db
│ └── standalone/
│ ├── Dockerfile
│ └── rootfs/
│ └── etc/
│ ├── caddy/
│ │ └── Caddyfile
│ └── s6-overlay/
│ ├── s6-rc.d/
│ │ ├── api/
│ │ │ ├── dependencies.d/
│ │ │ │ ├── init-db
│ │ │ │ └── redis
│ │ │ ├── run
│ │ │ └── type
│ │ ├── caddy/
│ │ │ ├── dependencies.d/
│ │ │ │ └── web
│ │ │ ├── run
│ │ │ └── type
│ │ ├── cron/
│ │ │ ├── dependencies.d/
│ │ │ │ ├── init-db
│ │ │ │ └── redis
│ │ │ ├── run
│ │ │ └── type
│ │ ├── init-db/
│ │ │ ├── dependencies.d/
│ │ │ │ └── postgres
│ │ │ ├── type
│ │ │ └── up
│ │ ├── postgres/
│ │ │ ├── dependencies.d/
│ │ │ │ └── base
│ │ │ ├── run
│ │ │ └── type
│ │ ├── redis/
│ │ │ ├── dependencies.d/
│ │ │ │ └── base
│ │ │ ├── run
│ │ │ └── type
│ │ ├── user/
│ │ │ ├── contents.d/
│ │ │ │ ├── api
│ │ │ │ ├── caddy
│ │ │ │ ├── cron
│ │ │ │ ├── init-db
│ │ │ │ ├── postgres
│ │ │ │ ├── redis
│ │ │ │ ├── web
│ │ │ │ └── worker
│ │ │ └── type
│ │ ├── web/
│ │ │ ├── dependencies.d/
│ │ │ │ └── api
│ │ │ ├── run
│ │ │ └── type
│ │ └── worker/
│ │ ├── dependencies.d/
│ │ │ ├── init-db
│ │ │ └── redis
│ │ ├── run
│ │ └── type
│ └── scripts/
│ └── init-db
├── knip.json
├── lefthook.yml
├── package.json
├── packages/
│ ├── auth/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── capabilities.ts
│ │ │ ├── index.ts
│ │ │ ├── mcp-config.ts
│ │ │ ├── plugins/
│ │ │ │ └── username-only/
│ │ │ │ ├── endpoints/
│ │ │ │ │ ├── sign-in.ts
│ │ │ │ │ └── sign-up.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils/
│ │ │ │ ├── config.ts
│ │ │ │ └── schema.ts
│ │ │ ├── polar-customer-delete.ts
│ │ │ └── runtime-environment.ts
│ │ ├── tests/
│ │ │ ├── capabilities.test.ts
│ │ │ ├── mcp-config.test.ts
│ │ │ └── polar-customer-delete.test.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── broadcast/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ ├── state.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── calendar/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── core/
│ │ │ │ ├── events/
│ │ │ │ │ ├── all-day.ts
│ │ │ │ │ ├── content-hash.ts
│ │ │ │ │ ├── events.ts
│ │ │ │ │ ├── identity.ts
│ │ │ │ │ ├── mappings.ts
│ │ │ │ │ └── recurrence.ts
│ │ │ │ ├── oauth/
│ │ │ │ │ ├── accounts.ts
│ │ │ │ │ ├── config.ts
│ │ │ │ │ ├── coordinated-refresher.ts
│ │ │ │ │ ├── create-source-provider.ts
│ │ │ │ │ ├── ensure-valid-token.ts
│ │ │ │ │ ├── error-classification.ts
│ │ │ │ │ ├── google.ts
│ │ │ │ │ ├── microsoft.ts
│ │ │ │ │ ├── providers.ts
│ │ │ │ │ ├── refresh-coordinator.ts
│ │ │ │ │ ├── source-provider.ts
│ │ │ │ │ ├── state.ts
│ │ │ │ │ ├── sync-token.ts
│ │ │ │ │ ├── sync-window.ts
│ │ │ │ │ └── token-provider.ts
│ │ │ │ ├── source/
│ │ │ │ │ ├── event-diff.ts
│ │ │ │ │ ├── sync-diagnostics.ts
│ │ │ │ │ └── write-event-states.ts
│ │ │ │ ├── sync/
│ │ │ │ │ ├── aggregate-runtime.ts
│ │ │ │ │ ├── aggregate-tracker.ts
│ │ │ │ │ ├── operations.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── sync-engine/
│ │ │ │ │ ├── flush.ts
│ │ │ │ │ ├── generation.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── ingest.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils/
│ │ │ │ ├── concurrency.ts
│ │ │ │ ├── error.ts
│ │ │ │ ├── rate-limiter.ts
│ │ │ │ └── redis-rate-limiter.ts
│ │ │ ├── ics/
│ │ │ │ ├── index.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils/
│ │ │ │ ├── create-snapshot.ts
│ │ │ │ ├── diff-events.ts
│ │ │ │ ├── fetch-adapter.ts
│ │ │ │ ├── normalize-timezone.ts
│ │ │ │ ├── parse-ics-calendar.ts
│ │ │ │ ├── parse-ics-events.ts
│ │ │ │ ├── pull-remote-calendar.ts
│ │ │ │ └── types.ts
│ │ │ ├── index.ts
│ │ │ ├── providers/
│ │ │ │ ├── caldav/
│ │ │ │ │ ├── destination/
│ │ │ │ │ │ └── provider.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── shared/
│ │ │ │ │ │ ├── client.ts
│ │ │ │ │ │ ├── digest-fetch.ts
│ │ │ │ │ │ ├── ics.ts
│ │ │ │ │ │ └── sync-window.ts
│ │ │ │ │ ├── source/
│ │ │ │ │ │ ├── auth-error-classification.ts
│ │ │ │ │ │ ├── fetch-adapter.ts
│ │ │ │ │ │ ├── provider.ts
│ │ │ │ │ │ ├── sync-window.ts
│ │ │ │ │ │ └── sync.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── fastmail/
│ │ │ │ │ ├── destination/
│ │ │ │ │ │ └── provider.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── source/
│ │ │ │ │ └── provider.ts
│ │ │ │ ├── google/
│ │ │ │ │ ├── destination/
│ │ │ │ │ │ ├── provider.ts
│ │ │ │ │ │ ├── serialize-event.ts
│ │ │ │ │ │ └── sync.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── shared/
│ │ │ │ │ │ ├── api.ts
│ │ │ │ │ │ ├── backoff.ts
│ │ │ │ │ │ ├── batch.ts
│ │ │ │ │ │ ├── date-time.ts
│ │ │ │ │ │ └── errors.ts
│ │ │ │ │ ├── source/
│ │ │ │ │ │ ├── fetch-adapter.ts
│ │ │ │ │ │ ├── provider.ts
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── utils/
│ │ │ │ │ │ ├── fetch-events.ts
│ │ │ │ │ │ └── list-calendars.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── icloud/
│ │ │ │ │ ├── destination/
│ │ │ │ │ │ └── provider.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── source/
│ │ │ │ │ └── provider.ts
│ │ │ │ └── outlook/
│ │ │ │ ├── destination/
│ │ │ │ │ ├── provider.ts
│ │ │ │ │ ├── serialize-event.ts
│ │ │ │ │ └── sync.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── shared/
│ │ │ │ │ ├── api.ts
│ │ │ │ │ ├── date-time.ts
│ │ │ │ │ └── errors.ts
│ │ │ │ ├── source/
│ │ │ │ │ ├── fetch-adapter.ts
│ │ │ │ │ ├── provider.ts
│ │ │ │ │ ├── types.ts
│ │ │ │ │ └── utils/
│ │ │ │ │ ├── fetch-events.ts
│ │ │ │ │ └── list-calendars.ts
│ │ │ │ └── types.ts
│ │ │ └── utils/
│ │ │ ├── registry/
│ │ │ │ ├── registry.ts
│ │ │ │ └── server.ts
│ │ │ └── safe-fetch.ts
│ │ ├── tests/
│ │ │ ├── core/
│ │ │ │ ├── events/
│ │ │ │ │ ├── content-hash.test.ts
│ │ │ │ │ ├── events.test.ts
│ │ │ │ │ ├── identity.test.ts
│ │ │ │ │ └── recurrence.test.ts
│ │ │ │ ├── oauth/
│ │ │ │ │ ├── ensure-valid-token.test.ts
│ │ │ │ │ ├── error-classification.test.ts
│ │ │ │ │ ├── google.test.ts
│ │ │ │ │ ├── refresh-coordinator.test.ts
│ │ │ │ │ ├── sync-token.test.ts
│ │ │ │ │ └── sync-window.test.ts
│ │ │ │ ├── source/
│ │ │ │ │ ├── event-diff.test.ts
│ │ │ │ │ ├── sync-diagnostics.test.ts
│ │ │ │ │ └── write-event-states.test.ts
│ │ │ │ ├── sync/
│ │ │ │ │ ├── aggregate-runtime.test.ts
│ │ │ │ │ ├── aggregate-tracker.test.ts
│ │ │ │ │ └── operations.test.ts
│ │ │ │ ├── sync-engine/
│ │ │ │ │ ├── index.test.ts
│ │ │ │ │ └── ingest.test.ts
│ │ │ │ └── utils/
│ │ │ │ └── rate-limiter.test.ts
│ │ │ ├── ics/
│ │ │ │ └── utils/
│ │ │ │ ├── diff-events-extended.test.ts
│ │ │ │ ├── diff-events.test.ts
│ │ │ │ ├── ics-fixtures.test.ts
│ │ │ │ ├── normalize-timezone.test.ts
│ │ │ │ ├── outlook-windows-timezone.test.ts
│ │ │ │ ├── parse-ics-calendar.test.ts
│ │ │ │ └── parse-ics-events.test.ts
│ │ │ ├── providers/
│ │ │ │ ├── caldav/
│ │ │ │ │ ├── destination/
│ │ │ │ │ │ └── provider.test.ts
│ │ │ │ │ ├── shared/
│ │ │ │ │ │ ├── ics-fixtures.test.ts
│ │ │ │ │ │ ├── ics.test.ts
│ │ │ │ │ │ └── sync-window.test.ts
│ │ │ │ │ └── source/
│ │ │ │ │ ├── auth-error-classification.test.ts
│ │ │ │ │ ├── fetch-adapter.test.ts
│ │ │ │ │ └── sync-window.test.ts
│ │ │ │ ├── google/
│ │ │ │ │ ├── destination/
│ │ │ │ │ │ ├── provider.test.ts
│ │ │ │ │ │ └── serialize-event.test.ts
│ │ │ │ │ ├── shared/
│ │ │ │ │ │ ├── backoff.test.ts
│ │ │ │ │ │ ├── batch.test.ts
│ │ │ │ │ │ └── errors.test.ts
│ │ │ │ │ └── source/
│ │ │ │ │ ├── fetch-adapter.test.ts
│ │ │ │ │ ├── provider.test.ts
│ │ │ │ │ └── utils/
│ │ │ │ │ └── fetch-events.test.ts
│ │ │ │ └── outlook/
│ │ │ │ ├── destination/
│ │ │ │ │ ├── provider.test.ts
│ │ │ │ │ └── serialize-event.test.ts
│ │ │ │ └── source/
│ │ │ │ ├── fetch-adapter.test.ts
│ │ │ │ ├── provider.test.ts
│ │ │ │ └── utils/
│ │ │ │ └── fetch-events.test.ts
│ │ │ └── utils/
│ │ │ └── safe-fetch.test.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── constants/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── constants/
│ │ │ │ ├── http.ts
│ │ │ │ ├── scopes.ts
│ │ │ │ └── time.ts
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── data-schemas/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── client.ts
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── database/
│ │ ├── drizzle/
│ │ │ ├── 0000_slimy_justice.sql
│ │ │ ├── 0001_complete_golden_guardian.sql
│ │ │ ├── 0002_striped_queen_noir.sql
│ │ │ ├── 0003_nervous_vulcan.sql
│ │ │ ├── 0004_strong_midnight.sql
│ │ │ ├── 0005_dusty_nomad.sql
│ │ │ ├── 0006_curious_orphan.sql
│ │ │ ├── 0007_heavy_pretty_boy.sql
│ │ │ ├── 0008_vengeful_azazel.sql
│ │ │ ├── 0009_daily_thor_girl.sql
│ │ │ ├── 0010_heavy_prima.sql
│ │ │ ├── 0011_round_gorilla_man.sql
│ │ │ ├── 0012_vengeful_thena.sql
│ │ │ ├── 0013_parallel_union_jack.sql
│ │ │ ├── 0014_modern_talon.sql
│ │ │ ├── 0015_unique_impossible_man.sql
│ │ │ ├── 0016_salty_nextwave.sql
│ │ │ ├── 0017_outstanding_eddie_brock.sql
│ │ │ ├── 0018_whole_loa.sql
│ │ │ ├── 0019_tearful_doctor_doom.sql
│ │ │ ├── 0020_huge_talon.sql
│ │ │ ├── 0021_icy_white_queen.sql
│ │ │ ├── 0022_lazy_avengers.sql
│ │ │ ├── 0023_lyrical_genesis.sql
│ │ │ ├── 0024_aberrant_wallop.sql
│ │ │ ├── 0025_powerful_sentinels.sql
│ │ │ ├── 0026_typical_impossible_man.sql
│ │ │ ├── 0027_loose_hydra.sql
│ │ │ ├── 0028_lush_sumo.sql
│ │ │ ├── 0029_huge_yellow_claw.sql
│ │ │ ├── 0030_youthful_speed.sql
│ │ │ ├── 0031_glorious_joshua_kane.sql
│ │ │ ├── 0032_dapper_patch.sql
│ │ │ ├── 0033_square_tomorrow_man.sql
│ │ │ ├── 0034_dumb_clanker.sql
│ │ │ ├── 0035_known_silk_fever.sql
│ │ │ ├── 0036_late_bastion.sql
│ │ │ ├── 0037_thankful_machine_man.sql
│ │ │ ├── 0038_military_radioactive_man.sql
│ │ │ ├── 0039_fat_mad_thinker.sql
│ │ │ ├── 0040_sparkling_toad.sql
│ │ │ ├── 0041_keen_black_panther.sql
│ │ │ ├── 0042_famous_obadiah_stane.sql
│ │ │ ├── 0043_smart_demogoblin.sql
│ │ │ ├── 0044_crazy_kate_bishop.sql
│ │ │ ├── 0045_flippant_paper_doll.sql
│ │ │ ├── 0046_rainy_steve_rogers.sql
│ │ │ ├── 0047_soft_ravenous.sql
│ │ │ ├── 0048_gigantic_kid_colt.sql
│ │ │ ├── 0049_handy_sentinels.sql
│ │ │ ├── 0050_purple_patch.sql
│ │ │ ├── 0051_normal_mentallo.sql
│ │ │ ├── 0052_military_trish_tilby.sql
│ │ │ ├── 0053_greedy_reptil.sql
│ │ │ ├── 0054_nasty_sage.sql
│ │ │ ├── 0055_zippy_wolfsbane.sql
│ │ │ ├── 0056_ambiguous_unus.sql
│ │ │ ├── 0057_requeue_source_backfill.sql
│ │ │ ├── 0058_same_robbie_robertson.sql
│ │ │ ├── 0059_shocking_stone_men.sql
│ │ │ ├── 0060_condemned_imperial_guard.sql
│ │ │ ├── 0061_brief_toxin.sql
│ │ │ ├── 0062_lame_white_tiger.sql
│ │ │ ├── 0063_friendly_black_panther.sql
│ │ │ ├── 0064_talented_black_knight.sql
│ │ │ ├── 0065_dizzy_zarda.sql
│ │ │ ├── 0066_nasty_bushwacker.sql
│ │ │ ├── 0067_curvy_mole_man.sql
│ │ │ ├── 0068_clumsy_starbolt.sql
│ │ │ ├── 0069_amazing_storm.sql
│ │ │ └── meta/
│ │ │ ├── 0000_snapshot.json
│ │ │ ├── 0001_snapshot.json
│ │ │ ├── 0002_snapshot.json
│ │ │ ├── 0003_snapshot.json
│ │ │ ├── 0004_snapshot.json
│ │ │ ├── 0005_snapshot.json
│ │ │ ├── 0006_snapshot.json
│ │ │ ├── 0007_snapshot.json
│ │ │ ├── 0008_snapshot.json
│ │ │ ├── 0009_snapshot.json
│ │ │ ├── 0010_snapshot.json
│ │ │ ├── 0011_snapshot.json
│ │ │ ├── 0012_snapshot.json
│ │ │ ├── 0013_snapshot.json
│ │ │ ├── 0014_snapshot.json
│ │ │ ├── 0015_snapshot.json
│ │ │ ├── 0016_snapshot.json
│ │ │ ├── 0017_snapshot.json
│ │ │ ├── 0018_snapshot.json
│ │ │ ├── 0019_snapshot.json
│ │ │ ├── 0020_snapshot.json
│ │ │ ├── 0021_snapshot.json
│ │ │ ├── 0022_snapshot.json
│ │ │ ├── 0023_snapshot.json
│ │ │ ├── 0024_snapshot.json
│ │ │ ├── 0025_snapshot.json
│ │ │ ├── 0026_snapshot.json
│ │ │ ├── 0027_snapshot.json
│ │ │ ├── 0028_snapshot.json
│ │ │ ├── 0029_snapshot.json
│ │ │ ├── 0030_snapshot.json
│ │ │ ├── 0031_snapshot.json
│ │ │ ├── 0032_snapshot.json
│ │ │ ├── 0033_snapshot.json
│ │ │ ├── 0034_snapshot.json
│ │ │ ├── 0035_snapshot.json
│ │ │ ├── 0036_snapshot.json
│ │ │ ├── 0037_snapshot.json
│ │ │ ├── 0038_snapshot.json
│ │ │ ├── 0039_snapshot.json
│ │ │ ├── 0040_snapshot.json
│ │ │ ├── 0041_snapshot.json
│ │ │ ├── 0042_snapshot.json
│ │ │ ├── 0043_snapshot.json
│ │ │ ├── 0044_snapshot.json
│ │ │ ├── 0045_snapshot.json
│ │ │ ├── 0046_snapshot.json
│ │ │ ├── 0047_snapshot.json
│ │ │ ├── 0048_snapshot.json
│ │ │ ├── 0049_snapshot.json
│ │ │ ├── 0050_snapshot.json
│ │ │ ├── 0051_snapshot.json
│ │ │ ├── 0052_snapshot.json
│ │ │ ├── 0053_snapshot.json
│ │ │ ├── 0054_snapshot.json
│ │ │ ├── 0055_snapshot.json
│ │ │ ├── 0056_snapshot.json
│ │ │ ├── 0057_snapshot.json
│ │ │ ├── 0058_snapshot.json
│ │ │ ├── 0059_snapshot.json
│ │ │ ├── 0060_snapshot.json
│ │ │ ├── 0061_snapshot.json
│ │ │ ├── 0062_snapshot.json
│ │ │ ├── 0063_snapshot.json
│ │ │ ├── 0064_snapshot.json
│ │ │ ├── 0065_snapshot.json
│ │ │ ├── 0066_snapshot.json
│ │ │ ├── 0067_snapshot.json
│ │ │ ├── 0068_snapshot.json
│ │ │ ├── 0069_snapshot.json
│ │ │ └── _journal.json
│ │ ├── drizzle.config.ts
│ │ ├── package.json
│ │ ├── scripts/
│ │ │ └── migrate.ts
│ │ ├── src/
│ │ │ ├── database/
│ │ │ │ ├── auth-schema.ts
│ │ │ │ └── schema.ts
│ │ │ ├── encryption.ts
│ │ │ ├── index.ts
│ │ │ └── utils/
│ │ │ └── database.ts
│ │ ├── tests/
│ │ │ └── database/
│ │ │ └── auth-schema.test.ts
│ │ ├── tsconfig.json
│ │ ├── turbo.json
│ │ └── vitest.config.ts
│ ├── digest-fetch/
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── fixtures/
│ │ ├── ics/
│ │ │ ├── berkeley-ib-seminars.ics
│ │ │ ├── calendarlabs-us-holidays.ics
│ │ │ ├── google-canada-holidays.ics
│ │ │ ├── google-us-holidays.ics
│ │ │ ├── govuk-bank-holidays-england-wales.ics
│ │ │ ├── hebcal-geoname-3448439.ics
│ │ │ ├── meetup-ny-tech.ics
│ │ │ ├── meetup-torontojs.ics
│ │ │ ├── outlook-exchange-windows-timezones.ics
│ │ │ └── stanford-featured-events.ics
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── cache.ts
│ │ │ ├── index.ts
│ │ │ ├── manifest.ts
│ │ │ ├── schema.ts
│ │ │ └── scripts/
│ │ │ ├── sync-fixtures.ts
│ │ │ └── verify-fixtures.ts
│ │ └── tsconfig.json
│ ├── otelemetry/
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── premium/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── constants.ts
│ │ │ ├── index.ts
│ │ │ └── subscription.ts
│ │ └── tsconfig.json
│ ├── queue/
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── sync/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── destination-errors.ts
│ │ │ ├── index.ts
│ │ │ ├── resolve-provider.ts
│ │ │ ├── sync-lock.ts
│ │ │ └── sync-user.ts
│ │ ├── tests/
│ │ │ ├── destination-errors.test.ts
│ │ │ └── sync-lock.test.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ └── typescript-config/
│ ├── package.json
│ └── tsconfig.json
├── scripts/
│ └── bun-test.ts
├── services/
│ ├── api/
│ │ ├── Dockerfile
│ │ ├── entrypoint.sh
│ │ ├── package.json
│ │ ├── scripts/
│ │ │ └── build.ts
│ │ ├── src/
│ │ │ ├── context.ts
│ │ │ ├── env.ts
│ │ │ ├── handlers/
│ │ │ │ ├── auth-oauth-resource.ts
│ │ │ │ ├── auth.ts
│ │ │ │ ├── websocket-initial-status.ts
│ │ │ │ ├── websocket-payload.ts
│ │ │ │ └── websocket.ts
│ │ │ ├── index.ts
│ │ │ ├── middleware/
│ │ │ │ └── cors.ts
│ │ │ ├── mutations/
│ │ │ │ ├── index.ts
│ │ │ │ ├── providers/
│ │ │ │ │ ├── caldav.ts
│ │ │ │ │ ├── google.ts
│ │ │ │ │ └── outlook.ts
│ │ │ │ └── resolve-credentials.ts
│ │ │ ├── provider-display.ts
│ │ │ ├── queries/
│ │ │ │ ├── get-event-count.ts
│ │ │ │ ├── get-event.ts
│ │ │ │ ├── get-events-in-range.ts
│ │ │ │ ├── get-sync-statuses.ts
│ │ │ │ ├── list-destinations.ts
│ │ │ │ ├── list-mappings.ts
│ │ │ │ └── list-sources.ts
│ │ │ ├── read-models.ts
│ │ │ ├── routes/
│ │ │ │ └── api/
│ │ │ │ ├── accounts/
│ │ │ │ │ ├── [id].ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── cal/
│ │ │ │ │ └── [identifier].ts
│ │ │ │ ├── destinations/
│ │ │ │ │ ├── [id].ts
│ │ │ │ │ ├── authorize.ts
│ │ │ │ │ ├── caldav/
│ │ │ │ │ │ ├── discover.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── callback/
│ │ │ │ │ │ └── [provider].ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── entitlements.ts
│ │ │ │ ├── events/
│ │ │ │ │ ├── count.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── feedback/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── health.ts
│ │ │ │ ├── ical/
│ │ │ │ │ ├── settings.ts
│ │ │ │ │ └── token.ts
│ │ │ │ ├── ics/
│ │ │ │ │ ├── [id]/
│ │ │ │ │ │ └── destinations.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── source-routes.ts
│ │ │ │ ├── mappings/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── socket/
│ │ │ │ │ ├── token.ts
│ │ │ │ │ └── url.ts
│ │ │ │ ├── sources/
│ │ │ │ │ ├── [id]/
│ │ │ │ │ │ ├── destinations.ts
│ │ │ │ │ │ ├── mapping-routes.ts
│ │ │ │ │ │ ├── source-item-routes.ts
│ │ │ │ │ │ └── sources.ts
│ │ │ │ │ ├── [id].ts
│ │ │ │ │ ├── authorize.ts
│ │ │ │ │ ├── caldav/
│ │ │ │ │ │ ├── discover.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── callback/
│ │ │ │ │ │ └── [provider].ts
│ │ │ │ │ ├── callback-state.ts
│ │ │ │ │ ├── google/
│ │ │ │ │ │ ├── [id]/
│ │ │ │ │ │ │ └── destinations.ts
│ │ │ │ │ │ ├── calendars.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── outlook/
│ │ │ │ │ ├── [id]/
│ │ │ │ │ │ └── destinations.ts
│ │ │ │ │ ├── calendars.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── sync/
│ │ │ │ │ └── status.ts
│ │ │ │ ├── tokens/
│ │ │ │ │ ├── [id].ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── v1/
│ │ │ │ │ ├── accounts/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── calendars/
│ │ │ │ │ │ ├── [calendarId]/
│ │ │ │ │ │ │ └── invites.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── events/
│ │ │ │ │ │ ├── [id].ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── ical/
│ │ │ │ │ └── index.ts
│ │ │ │ └── webhook/
│ │ │ │ └── polar.ts
│ │ │ ├── types.ts
│ │ │ └── utils/
│ │ │ ├── api-rate-limit.ts
│ │ │ ├── api-tokens.ts
│ │ │ ├── background-task.ts
│ │ │ ├── caldav-sources.ts
│ │ │ ├── caldav.ts
│ │ │ ├── date-range.ts
│ │ │ ├── destinations.ts
│ │ │ ├── enqueue-push-sync.ts
│ │ │ ├── ical-format.ts
│ │ │ ├── ical.ts
│ │ │ ├── invalidate-calendars.ts
│ │ │ ├── logging.ts
│ │ │ ├── middleware.ts
│ │ │ ├── oauth-calendar-listing.ts
│ │ │ ├── oauth-callback-state.ts
│ │ │ ├── oauth-refresh.ts
│ │ │ ├── oauth-source-credentials.ts
│ │ │ ├── oauth-sources.ts
│ │ │ ├── oauth.ts
│ │ │ ├── provider-display.ts
│ │ │ ├── request-body.ts
│ │ │ ├── request-query.ts
│ │ │ ├── responses.ts
│ │ │ ├── route-handler.ts
│ │ │ ├── safe-fetch-options.ts
│ │ │ ├── source-destination-mappings.ts
│ │ │ ├── source-lifecycle.ts
│ │ │ ├── source-sync-defaults.ts
│ │ │ ├── sources.ts
│ │ │ ├── state.ts
│ │ │ └── user.ts
│ │ ├── tests/
│ │ │ ├── handlers/
│ │ │ │ ├── auth-oauth-resource.test.ts
│ │ │ │ ├── auth.test.ts
│ │ │ │ ├── websocket-initial-status.test.ts
│ │ │ │ └── websocket-payload.test.ts
│ │ │ ├── routes/
│ │ │ │ └── api/
│ │ │ │ ├── ical/
│ │ │ │ │ └── settings.test.ts
│ │ │ │ ├── ics/
│ │ │ │ │ └── source-routes.test.ts
│ │ │ │ └── sources/
│ │ │ │ └── [id]/
│ │ │ │ ├── mapping-routes.test.ts
│ │ │ │ └── source-item-routes.test.ts
│ │ │ └── utils/
│ │ │ ├── account-locks.test.ts
│ │ │ ├── api-rate-limit.test.ts
│ │ │ ├── enqueue-push-sync.test.ts
│ │ │ ├── ical.test.ts
│ │ │ ├── oauth-sources.test.ts
│ │ │ ├── oauth.test.ts
│ │ │ ├── request-body.test.ts
│ │ │ ├── source-destination-mappings.test.ts
│ │ │ ├── source-lifecycle.test.ts
│ │ │ └── source-sync-defaults.test.ts
│ │ ├── tsconfig.json
│ │ ├── turbo.json
│ │ └── vitest.config.ts
│ ├── cron/
│ │ ├── Dockerfile
│ │ ├── entrypoint.sh
│ │ ├── package.json
│ │ ├── scripts/
│ │ │ └── build.ts
│ │ ├── src/
│ │ │ ├── context.ts
│ │ │ ├── env.ts
│ │ │ ├── index.ts
│ │ │ ├── jobs/
│ │ │ │ ├── ingest-sources.ts
│ │ │ │ ├── push-destinations.ts
│ │ │ │ └── reconcile-subscriptions.ts
│ │ │ ├── migration-check.ts
│ │ │ └── utils/
│ │ │ ├── baker.ts
│ │ │ ├── get-jobs.ts
│ │ │ ├── get-sources.ts
│ │ │ ├── inject-jobs.ts
│ │ │ ├── logging.ts
│ │ │ ├── register-jobs.ts
│ │ │ ├── safe-fetch-options.ts
│ │ │ ├── source-plan-selection.ts
│ │ │ └── with-wide-event.ts
│ │ ├── tests/
│ │ │ ├── jobs/
│ │ │ │ └── reconcile-subscriptions.test.ts
│ │ │ ├── migration-check.test.ts
│ │ │ └── utils/
│ │ │ ├── get-jobs.test.ts
│ │ │ └── source-plan-selection.test.ts
│ │ ├── tsconfig.json
│ │ ├── turbo.json
│ │ └── vitest.config.ts
│ ├── mcp/
│ │ ├── Dockerfile
│ │ ├── entrypoint.sh
│ │ ├── package.json
│ │ ├── scripts/
│ │ │ └── build.ts
│ │ ├── src/
│ │ │ ├── context.ts
│ │ │ ├── env.ts
│ │ │ ├── index.ts
│ │ │ ├── mcp-handler.ts
│ │ │ ├── routes/
│ │ │ │ ├── health.ts
│ │ │ │ └── mcp.ts
│ │ │ ├── toolset.ts
│ │ │ └── utils/
│ │ │ ├── logging.ts
│ │ │ ├── middleware.ts
│ │ │ └── route-handler.ts
│ │ ├── tests/
│ │ │ ├── mcp-handler.test.ts
│ │ │ └── toolset.test.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ └── worker/
│ ├── Dockerfile
│ ├── entrypoint.sh
│ ├── package.json
│ ├── scripts/
│ │ └── build.ts
│ ├── src/
│ │ ├── context.ts
│ │ ├── env.ts
│ │ ├── index.ts
│ │ ├── processor.ts
│ │ └── utils/
│ │ └── logging.ts
│ └── tsconfig.json
└── turbo.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .claude/settings.json
================================================
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bun unused"
},
{
"type": "command",
"command": "bun lint"
},
{
"type": "command",
"command": "bun types"
}
]
}
]
}
}
================================================
FILE: .claude/skills/react-best-practices/AGENTS.md
================================================
# React Best Practices
**Version 0.1.0**
Vercel Engineering
January 2026
> **Note:**
> This document is mainly for agents and LLMs to follow when maintaining,
> generating, or refactoring React and Next.js codebases at Vercel. Humans
> may also find it useful, but guidance here is optimized for automation
> and consistency by AI-assisted workflows.
---
## Abstract
Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.
---
## Table of Contents
1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL**
- 1.1 [Defer Await Until Needed](#11-defer-await-until-needed)
- 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization)
- 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes)
- 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations)
- 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries)
2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL**
- 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports)
- 2.2 [Conditional Module Loading](#22-conditional-module-loading)
- 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries)
- 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)
- 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)
3. [Server-Side Performance](#3-server-side-performance) — **HIGH**
- 3.1 [Cross-Request LRU Caching](#31-cross-request-lru-caching)
- 3.2 [Minimize Serialization at RSC Boundaries](#32-minimize-serialization-at-rsc-boundaries)
- 3.3 [Parallel Data Fetching with Component Composition](#33-parallel-data-fetching-with-component-composition)
- 3.4 [Per-Request Deduplication with React.cache()](#34-per-request-deduplication-with-reactcache)
- 3.5 [Use after() for Non-Blocking Operations](#35-use-after-for-non-blocking-operations)
4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**
- 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)
- 4.2 [Use SWR for Automatic Deduplication](#42-use-swr-for-automatic-deduplication)
5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**
- 5.1 [Defer State Reads to Usage Point](#51-defer-state-reads-to-usage-point)
- 5.2 [Extract to Memoized Components](#52-extract-to-memoized-components)
- 5.3 [Narrow Effect Dependencies](#53-narrow-effect-dependencies)
- 5.4 [Subscribe to Derived State](#54-subscribe-to-derived-state)
- 5.5 [Use Functional setState Updates](#55-use-functional-setstate-updates)
- 5.6 [Use Lazy State Initialization](#56-use-lazy-state-initialization)
- 5.7 [Use Transitions for Non-Urgent Updates](#57-use-transitions-for-non-urgent-updates)
6. [Rendering Performance](#6-rendering-performance) — **MEDIUM**
- 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element)
- 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists)
- 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)
- 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)
- 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering)
- 6.6 [Use Activity Component for Show/Hide](#66-use-activity-component-for-showhide)
- 6.7 [Use Explicit Conditional Rendering](#67-use-explicit-conditional-rendering)
7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**
- 7.1 [Batch DOM CSS Changes](#71-batch-dom-css-changes)
- 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups)
- 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)
- 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)
- 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls)
- 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations)
- 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons)
- 7.8 [Early Return from Functions](#78-early-return-from-functions)
- 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation)
- 7.10 [Use Loop for Min/Max Instead of Sort](#710-use-loop-for-minmax-instead-of-sort)
- 7.11 [Use Set/Map for O(1) Lookups](#711-use-setmap-for-o1-lookups)
- 7.12 [Use toSorted() Instead of sort() for Immutability](#712-use-tosorted-instead-of-sort-for-immutability)
8. [Advanced Patterns](#8-advanced-patterns) — **LOW**
- 8.1 [Store Event Handlers in Refs](#81-store-event-handlers-in-refs)
- 8.2 [useLatest for Stable Callback Refs](#82-uselatest-for-stable-callback-refs)
---
## 1. Eliminating Waterfalls
**Impact: CRITICAL**
Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
### 1.1 Defer Await Until Needed
**Impact: HIGH (avoids blocking unused code paths)**
Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
**Incorrect: blocks both branches**
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId)
if (skipProcessing) {
// Returns immediately but still waited for userData
return { skipped: true }
}
// Only this branch uses userData
return processUserData(userData)
}
```
**Correct: only blocks when needed**
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) {
// Returns immediately without waiting
return { skipped: true }
}
// Fetch only when needed
const userData = await fetchUserData(userId)
return processUserData(userData)
}
```
**Another example: early return optimization**
```typescript
// Incorrect: always fetches permissions
async function updateResource(resourceId: string, userId: string) {
const permissions = await fetchPermissions(userId)
const resource = await getResource(resourceId)
if (!resource) {
return { error: 'Not found' }
}
if (!permissions.canEdit) {
return { error: 'Forbidden' }
}
return await updateResourceData(resource, permissions)
}
// Correct: fetches only when needed
async function updateResource(resourceId: string, userId: string) {
const resource = await getResource(resourceId)
if (!resource) {
return { error: 'Not found' }
}
const permissions = await fetchPermissions(userId)
if (!permissions.canEdit) {
return { error: 'Forbidden' }
}
return await updateResourceData(resource, permissions)
}
```
This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
### 1.2 Dependency-Based Parallelization
**Impact: CRITICAL (2-10× improvement)**
For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
**Incorrect: profile waits for config unnecessarily**
```typescript
const [user, config] = await Promise.all([
fetchUser(),
fetchConfig()
])
const profile = await fetchProfile(user.id)
```
**Correct: config and profile run in parallel**
```typescript
import { all } from 'better-all'
const { user, config, profile } = await all({
async user() { return fetchUser() },
async config() { return fetchConfig() },
async profile() {
return fetchProfile((await this.$.user).id)
}
})
```
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
### 1.3 Prevent Waterfall Chains in API Routes
**Impact: CRITICAL (2-10× improvement)**
In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
**Incorrect: config waits for auth, data waits for both**
```typescript
export async function GET(request: Request) {
const session = await auth()
const config = await fetchConfig()
const data = await fetchData(session.user.id)
return Response.json({ data, config })
}
```
**Correct: auth and config start immediately**
```typescript
export async function GET(request: Request) {
const sessionPromise = auth()
const configPromise = fetchConfig()
const session = await sessionPromise
const [config, data] = await Promise.all([
configPromise,
fetchData(session.user.id)
])
return Response.json({ data, config })
}
```
For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
### 1.4 Promise.all() for Independent Operations
**Impact: CRITICAL (2-10× improvement)**
When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
**Incorrect: sequential execution, 3 round trips**
```typescript
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
```
**Correct: parallel execution, 1 round trip**
```typescript
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
])
```
### 1.5 Strategic Suspense Boundaries
**Impact: HIGH (faster initial paint)**
Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
**Incorrect: wrapper blocked by data fetching**
```tsx
async function Page() {
const data = await fetchData() // Blocks entire page
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<div>
<DataDisplay data={data} />
</div>
<div>Footer</div>
</div>
)
}
```
The entire layout waits for data even though only the middle section needs it.
**Correct: wrapper shows immediately, data streams in**
```tsx
function Page() {
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<div>
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>
</div>
<div>Footer</div>
</div>
)
}
async function DataDisplay() {
const data = await fetchData() // Only blocks this component
return <div>{data.content}</div>
}
```
Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
**Alternative: share promise across components**
```tsx
function Page() {
// Start fetch immediately, but don't await
const dataPromise = fetchData()
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<Suspense fallback={<Skeleton />}>
<DataDisplay dataPromise={dataPromise} />
<DataSummary dataPromise={dataPromise} />
</Suspense>
<div>Footer</div>
</div>
)
}
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // Unwraps the promise
return <div>{data.content}</div>
}
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // Reuses the same promise
return <div>{data.summary}</div>
}
```
Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
**When NOT to use this pattern:**
- Critical data needed for layout decisions (affects positioning)
- SEO-critical content above the fold
- Small, fast queries where suspense overhead isn't worth it
- When you want to avoid layout shift (loading → content jump)
**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
---
## 2. Bundle Size Optimization
**Impact: CRITICAL**
Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
### 2.1 Avoid Barrel File Imports
**Impact: CRITICAL (200-800ms import cost, slow builds)**
Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.
**Incorrect: imports entire library**
```tsx
import { Check, X, Menu } from 'lucide-react'
// Loads 1,583 modules, takes ~2.8s extra in dev
// Runtime cost: 200-800ms on every cold start
import { Button, TextField } from '@mui/material'
// Loads 2,225 modules, takes ~4.2s extra in dev
```
**Correct: imports only what you need**
```tsx
import Check from 'lucide-react/dist/esm/icons/check'
import X from 'lucide-react/dist/esm/icons/x'
import Menu from 'lucide-react/dist/esm/icons/menu'
// Loads only 3 modules (~2KB vs ~1MB)
import Button from '@mui/material/Button'
import TextField from '@mui/material/TextField'
// Loads only what you use
```
**Alternative: Next.js 13.5+**
```js
// next.config.js - use optimizePackageImports
module.exports = {
experimental: {
optimizePackageImports: ['lucide-react', '@mui/material']
}
}
// Then you can keep the ergonomic barrel imports:
import { Check, X, Menu } from 'lucide-react'
// Automatically transformed to direct imports at build time
```
Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.
Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
### 2.2 Conditional Module Loading
**Impact: HIGH (loads large data only when needed)**
Load large data or modules only when a feature is activated.
**Example: lazy-load animation frames**
```tsx
function AnimationPlayer({ enabled }: { enabled: boolean }) {
const [frames, setFrames] = useState<Frame[] | null>(null)
useEffect(() => {
if (enabled && !frames && typeof window !== 'undefined') {
import('./animation-frames.js')
.then(mod => setFrames(mod.frames))
.catch(() => setEnabled(false))
}
}, [enabled, frames])
if (!frames) return <Skeleton />
return <Canvas frames={frames} />
}
```
The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
### 2.3 Defer Non-Critical Third-Party Libraries
**Impact: MEDIUM (loads after hydration)**
Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
**Incorrect: blocks initial bundle**
```tsx
import { Analytics } from '@vercel/analytics/react'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
)
}
```
**Correct: loads after hydration**
```tsx
import dynamic from 'next/dynamic'
const Analytics = dynamic(
() => import('@vercel/analytics/react').then(m => m.Analytics),
{ ssr: false }
)
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
)
}
```
### 2.4 Dynamic Imports for Heavy Components
**Impact: CRITICAL (directly affects TTI and LCP)**
Use `next/dynamic` to lazy-load large components not needed on initial render.
**Incorrect: Monaco bundles with main chunk ~300KB**
```tsx
import { MonacoEditor } from './monaco-editor'
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />
}
```
**Correct: Monaco loads on demand**
```tsx
import dynamic from 'next/dynamic'
const MonacoEditor = dynamic(
() => import('./monaco-editor').then(m => m.MonacoEditor),
{ ssr: false }
)
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />
}
```
### 2.5 Preload Based on User Intent
**Impact: MEDIUM (reduces perceived latency)**
Preload heavy bundles before they're needed to reduce perceived latency.
**Example: preload on hover/focus**
```tsx
function EditorButton({ onClick }: { onClick: () => void }) {
const preload = () => {
if (typeof window !== 'undefined') {
void import('./monaco-editor')
}
}
return (
<button
onMouseEnter={preload}
onFocus={preload}
onClick={onClick}
>
Open Editor
</button>
)
}
```
**Example: preload when feature flag is enabled**
```tsx
function FlagsProvider({ children, flags }: Props) {
useEffect(() => {
if (flags.editorEnabled && typeof window !== 'undefined') {
void import('./monaco-editor').then(mod => mod.init())
}
}, [flags.editorEnabled])
return <FlagsContext.Provider value={flags}>
{children}
</FlagsContext.Provider>
}
```
The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
---
## 3. Server-Side Performance
**Impact: HIGH**
Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
### 3.1 Cross-Request LRU Caching
**Impact: HIGH (caches across requests)**
`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
**Implementation:**
```typescript
import { LRUCache } from 'lru-cache'
const cache = new LRUCache<string, any>({
max: 1000,
ttl: 5 * 60 * 1000 // 5 minutes
})
export async function getUser(id: string) {
const cached = cache.get(id)
if (cached) return cached
const user = await db.user.findUnique({ where: { id } })
cache.set(id, user)
return user
}
// Request 1: DB query, result cached
// Request 2: cache hit, no DB query
```
Use when sequential user actions hit multiple endpoints needing the same data within seconds.
**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
### 3.2 Minimize Serialization at RSC Boundaries
**Impact: HIGH (reduces data transfer size)**
The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.
**Incorrect: serializes all 50 fields**
```tsx
async function Page() {
const user = await fetchUser() // 50 fields
return <Profile user={user} />
}
'use client'
function Profile({ user }: { user: User }) {
return <div>{user.name}</div> // uses 1 field
}
```
**Correct: serializes only 1 field**
```tsx
async function Page() {
const user = await fetchUser()
return <Profile name={user.name} />
}
'use client'
function Profile({ name }: { name: string }) {
return <div>{name}</div>
}
```
### 3.3 Parallel Data Fetching with Component Composition
**Impact: CRITICAL (eliminates server-side waterfalls)**
React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
**Incorrect: Sidebar waits for Page's fetch to complete**
```tsx
export default async function Page() {
const header = await fetchHeader()
return (
<div>
<div>{header}</div>
<Sidebar />
</div>
)
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
}
```
**Correct: both fetch simultaneously**
```tsx
async function Header() {
const data = await fetchHeader()
return <div>{data}</div>
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
}
export default function Page() {
return (
<div>
<Header />
<Sidebar />
</div>
)
}
```
**Alternative with children prop:**
```tsx
async function Layout({ children }: { children: ReactNode }) {
const header = await fetchHeader()
return (
<div>
<div>{header}</div>
{children}
</div>
)
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
}
export default function Page() {
return (
<Layout>
<Sidebar />
</Layout>
)
}
```
### 3.4 Per-Request Deduplication with React.cache()
**Impact: MEDIUM (deduplicates within request)**
Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
**Usage:**
```typescript
import { cache } from 'react'
export const getCurrentUser = cache(async () => {
const session = await auth()
if (!session?.user?.id) return null
return await db.user.findUnique({
where: { id: session.user.id }
})
})
```
Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
### 3.5 Use after() for Non-Blocking Operations
**Impact: MEDIUM (faster response times)**
Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.
**Incorrect: blocks response**
```tsx
import { logUserAction } from '@/app/utils'
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
// Logging blocks the response
const userAgent = request.headers.get('user-agent') || 'unknown'
await logUserAction({ userAgent })
return new Response(JSON.stringify({ status: 'success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}
```
**Correct: non-blocking**
```tsx
import { after } from 'next/server'
import { headers, cookies } from 'next/headers'
import { logUserAction } from '@/app/utils'
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
// Log after response is sent
after(async () => {
const userAgent = (await headers()).get('user-agent') || 'unknown'
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
logUserAction({ sessionCookie, userAgent })
})
return new Response(JSON.stringify({ status: 'success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}
```
The response is sent immediately while logging happens in the background.
**Common use cases:**
- Analytics tracking
- Audit logging
- Sending notifications
- Cache invalidation
- Cleanup tasks
**Important notes:**
- `after()` runs even if the response fails or redirects
- Works in Server Actions, Route Handlers, and Server Components
Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
---
## 4. Client-Side Data Fetching
**Impact: MEDIUM-HIGH**
Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
### 4.1 Deduplicate Global Event Listeners
**Impact: LOW (single listener for N components)**
Use `useSWRSubscription()` to share global event listeners across component instances.
**Incorrect: N instances = N listeners**
```tsx
function useKeyboardShortcut(key: string, callback: () => void) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && e.key === key) {
callback()
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [key, callback])
}
```
When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
**Correct: N instances = 1 listener**
```tsx
import useSWRSubscription from 'swr/subscription'
// Module-level Map to track callbacks per key
const keyCallbacks = new Map<string, Set<() => void>>()
function useKeyboardShortcut(key: string, callback: () => void) {
// Register this callback in the Map
useEffect(() => {
if (!keyCallbacks.has(key)) {
keyCallbacks.set(key, new Set())
}
keyCallbacks.get(key)!.add(callback)
return () => {
const set = keyCallbacks.get(key)
if (set) {
set.delete(callback)
if (set.size === 0) {
keyCallbacks.delete(key)
}
}
}
}, [key, callback])
useSWRSubscription('global-keydown', () => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && keyCallbacks.has(e.key)) {
keyCallbacks.get(e.key)!.forEach(cb => cb())
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
})
}
function Profile() {
// Multiple shortcuts will share the same listener
useKeyboardShortcut('p', () => { /* ... */ })
useKeyboardShortcut('k', () => { /* ... */ })
// ...
}
```
### 4.2 Use SWR for Automatic Deduplication
**Impact: MEDIUM-HIGH (automatic deduplication)**
SWR enables request deduplication, caching, and revalidation across component instances.
**Incorrect: no deduplication, each instance fetches**
```tsx
function UserList() {
const [users, setUsers] = useState([])
useEffect(() => {
fetch('/api/users')
.then(r => r.json())
.then(setUsers)
}, [])
}
```
**Correct: multiple instances share one request**
```tsx
import useSWR from 'swr'
function UserList() {
const { data: users } = useSWR('/api/users', fetcher)
}
```
**For immutable data:**
```tsx
import { useImmutableSWR } from '@/lib/swr'
function StaticContent() {
const { data } = useImmutableSWR('/api/config', fetcher)
}
```
**For mutations:**
```tsx
import { useSWRMutation } from 'swr/mutation'
function UpdateButton() {
const { trigger } = useSWRMutation('/api/user', updateUser)
return <button onClick={() => trigger()}>Update</button>
}
```
Reference: [https://swr.vercel.app](https://swr.vercel.app)
---
## 5. Re-render Optimization
**Impact: MEDIUM**
Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
### 5.1 Defer State Reads to Usage Point
**Impact: MEDIUM (avoids unnecessary subscriptions)**
Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
**Incorrect: subscribes to all searchParams changes**
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const searchParams = useSearchParams()
const handleShare = () => {
const ref = searchParams.get('ref')
shareChat(chatId, { ref })
}
return <button onClick={handleShare}>Share</button>
}
```
**Correct: reads on demand, no subscription**
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const handleShare = () => {
const params = new URLSearchParams(window.location.search)
const ref = params.get('ref')
shareChat(chatId, { ref })
}
return <button onClick={handleShare}>Share</button>
}
```
### 5.2 Extract to Memoized Components
**Impact: MEDIUM (enables early returns)**
Extract expensive work into memoized components to enable early returns before computation.
**Incorrect: computes avatar even when loading**
```tsx
function Profile({ user, loading }: Props) {
const avatar = useMemo(() => {
const id = computeAvatarId(user)
return <Avatar id={id} />
}, [user])
if (loading) return <Skeleton />
return <div>{avatar}</div>
}
```
**Correct: skips computation when loading**
```tsx
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const id = useMemo(() => computeAvatarId(user), [user])
return <Avatar id={id} />
})
function Profile({ user, loading }: Props) {
if (loading) return <Skeleton />
return (
<div>
<UserAvatar user={user} />
</div>
)
}
```
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.
### 5.3 Narrow Effect Dependencies
**Impact: LOW (minimizes effect re-runs)**
Specify primitive dependencies instead of objects to minimize effect re-runs.
**Incorrect: re-runs on any user field change**
```tsx
useEffect(() => {
console.log(user.id)
}, [user])
```
**Correct: re-runs only when id changes**
```tsx
useEffect(() => {
console.log(user.id)
}, [user.id])
```
**For derived state, compute outside effect:**
```tsx
// Incorrect: runs on width=767, 766, 765...
useEffect(() => {
if (width < 768) {
enableMobileMode()
}
}, [width])
// Correct: runs only on boolean transition
const isMobile = width < 768
useEffect(() => {
if (isMobile) {
enableMobileMode()
}
}, [isMobile])
```
### 5.4 Subscribe to Derived State
**Impact: MEDIUM (reduces re-render frequency)**
Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
**Incorrect: re-renders on every pixel change**
```tsx
function Sidebar() {
const width = useWindowWidth() // updates continuously
const isMobile = width < 768
return <nav className={isMobile ? 'mobile' : 'desktop'}>
}
```
**Correct: re-renders only when boolean changes**
```tsx
function Sidebar() {
const isMobile = useMediaQuery('(max-width: 767px)')
return <nav className={isMobile ? 'mobile' : 'desktop'}>
}
```
### 5.5 Use Functional setState Updates
**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**
When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
**Incorrect: requires state as dependency**
```tsx
function TodoList() {
const [items, setItems] = useState(initialItems)
// Callback must depend on items, recreated on every items change
const addItems = useCallback((newItems: Item[]) => {
setItems([...items, ...newItems])
}, [items]) // ❌ items dependency causes recreations
// Risk of stale closure if dependency is forgotten
const removeItem = useCallback((id: string) => {
setItems(items.filter(item => item.id !== id))
}, []) // ❌ Missing items dependency - will use stale items!
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
}
```
The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
**Correct: stable callbacks, no stale closures**
```tsx
function TodoList() {
const [items, setItems] = useState(initialItems)
// Stable callback, never recreated
const addItems = useCallback((newItems: Item[]) => {
setItems(curr => [...curr, ...newItems])
}, []) // ✅ No dependencies needed
// Always uses latest state, no stale closure risk
const removeItem = useCallback((id: string) => {
setItems(curr => curr.filter(item => item.id !== id))
}, []) // ✅ Safe and stable
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
}
```
**Benefits:**
1. **Stable callback references** - Callbacks don't need to be recreated when state changes
2. **No stale closures** - Always operates on the latest state value
3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
4. **Prevents bugs** - Eliminates the most common source of React closure bugs
**When to use functional updates:**
- Any setState that depends on the current state value
- Inside useCallback/useMemo when state is needed
- Event handlers that reference state
- Async operations that update state
**When direct updates are fine:**
- Setting state to a static value: `setCount(0)`
- Setting state from props/arguments only: `setName(newName)`
- State doesn't depend on previous value
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
### 5.6 Use Lazy State Initialization
**Impact: MEDIUM (wasted computation on every render)**
Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
**Incorrect: runs on every render**
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs on EVERY render, even after initialization
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
const [query, setQuery] = useState('')
// When query changes, buildSearchIndex runs again unnecessarily
return <SearchResults index={searchIndex} query={query} />
}
function UserProfile() {
// JSON.parse runs on every render
const [settings, setSettings] = useState(
JSON.parse(localStorage.getItem('settings') || '{}')
)
return <SettingsForm settings={settings} onChange={setSettings} />
}
```
**Correct: runs only once**
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs ONLY on initial render
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
const [query, setQuery] = useState('')
return <SearchResults index={searchIndex} query={query} />
}
function UserProfile() {
// JSON.parse runs only on initial render
const [settings, setSettings] = useState(() => {
const stored = localStorage.getItem('settings')
return stored ? JSON.parse(stored) : {}
})
return <SettingsForm settings={settings} onChange={setSettings} />
}
```
Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
### 5.7 Use Transitions for Non-Urgent Updates
**Impact: MEDIUM (maintains UI responsiveness)**
Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
**Incorrect: blocks UI on every scroll**
```tsx
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
useEffect(() => {
const handler = () => setScrollY(window.scrollY)
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
}
```
**Correct: non-blocking updates**
```tsx
import { startTransition } from 'react'
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
useEffect(() => {
const handler = () => {
startTransition(() => setScrollY(window.scrollY))
}
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
}
```
---
## 6. Rendering Performance
**Impact: MEDIUM**
Optimizing the rendering process reduces the work the browser needs to do.
### 6.1 Animate SVG Wrapper Instead of SVG Element
**Impact: LOW (enables hardware acceleration)**
Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.
**Incorrect: animating SVG directly - no hardware acceleration**
```tsx
function LoadingSpinner() {
return (
<svg
className="animate-spin"
width="24"
height="24"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
)
}
```
**Correct: animating wrapper div - hardware accelerated**
```tsx
function LoadingSpinner() {
return (
<div className="animate-spin">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
</div>
)
}
```
This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
### 6.2 CSS content-visibility for Long Lists
**Impact: HIGH (faster initial render)**
Apply `content-visibility: auto` to defer off-screen rendering.
**CSS:**
```css
.message-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
```
**Example:**
```tsx
function MessageList({ messages }: { messages: Message[] }) {
return (
<div className="overflow-y-auto h-screen">
{messages.map(msg => (
<div key={msg.id} className="message-item">
<Avatar user={msg.author} />
<div>{msg.content}</div>
</div>
))}
</div>
)
}
```
For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
### 6.3 Hoist Static JSX Elements
**Impact: LOW (avoids re-creation)**
Extract static JSX outside components to avoid re-creation.
**Incorrect: recreates element every render**
```tsx
function LoadingSkeleton() {
return <div className="animate-pulse h-20 bg-gray-200" />
}
function Container() {
return (
<div>
{loading && <LoadingSkeleton />}
</div>
)
}
```
**Correct: reuses same element**
```tsx
const loadingSkeleton = (
<div className="animate-pulse h-20 bg-gray-200" />
)
function Container() {
return (
<div>
{loading && loadingSkeleton}
</div>
)
}
```
This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
### 6.4 Optimize SVG Precision
**Impact: LOW (reduces file size)**
Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
**Incorrect: excessive precision**
```svg
<path d="M 10.293847 20.847362 L 30.938472 40.192837" />
```
**Correct: 1 decimal place**
```svg
<path d="M 10.3 20.8 L 30.9 40.2" />
```
**Automate with SVGO:**
```bash
npx svgo --precision=1 --multipass icon.svg
```
### 6.5 Prevent Hydration Mismatch Without Flickering
**Impact: MEDIUM (avoids visual flicker and hydration errors)**
When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
**Incorrect: breaks SSR**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
// localStorage is not available on server - throws error
const theme = localStorage.getItem('theme') || 'light'
return (
<div className={theme}>
{children}
</div>
)
}
```
Server-side rendering will fail because `localStorage` is undefined.
**Incorrect: visual flickering**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState('light')
useEffect(() => {
// Runs after hydration - causes visible flash
const stored = localStorage.getItem('theme')
if (stored) {
setTheme(stored)
}
}, [])
return (
<div className={theme}>
{children}
</div>
)
}
```
Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
**Correct: no flicker, no hydration mismatch**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
return (
<>
<div id="theme-wrapper">
{children}
</div>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
var theme = localStorage.getItem('theme') || 'light';
var el = document.getElementById('theme-wrapper');
if (el) el.className = theme;
} catch (e) {}
})();
`,
}}
/>
</>
)
}
```
The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
### 6.6 Use Activity Component for Show/Hide
**Impact: MEDIUM (preserves state/DOM)**
Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.
**Usage:**
```tsx
import { Activity } from 'react'
function Dropdown({ isOpen }: Props) {
return (
<Activity mode={isOpen ? 'visible' : 'hidden'}>
<ExpensiveMenu />
</Activity>
)
}
```
Avoids expensive re-renders and state loss.
### 6.7 Use Explicit Conditional Rendering
**Impact: LOW (prevents rendering 0 or NaN)**
Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
**Incorrect: renders "0" when count is 0**
```tsx
function Badge({ count }: { count: number }) {
return (
<div>
{count && <span className="badge">{count}</span>}
</div>
)
}
// When count = 0, renders: <div>0</div>
// When count = 5, renders: <div><span class="badge">5</span></div>
```
**Correct: renders nothing when count is 0**
```tsx
function Badge({ count }: { count: number }) {
return (
<div>
{count > 0 ? <span className="badge">{count}</span> : null}
</div>
)
}
// When count = 0, renders: <div></div>
// When count = 5, renders: <div><span class="badge">5</span></div>
```
---
## 7. JavaScript Performance
**Impact: LOW-MEDIUM**
Micro-optimizations for hot paths can add up to meaningful improvements.
### 7.1 Batch DOM CSS Changes
**Impact: MEDIUM (reduces reflows/repaints)**
Avoid changing styles one property at a time. Group multiple CSS changes together via classes or `cssText` to minimize browser reflows.
**Incorrect: multiple reflows**
```typescript
function updateElementStyles(element: HTMLElement) {
// Each line triggers a reflow
element.style.width = '100px'
element.style.height = '200px'
element.style.backgroundColor = 'blue'
element.style.border = '1px solid black'
}
```
**Correct: add class - single reflow**
```typescript
// CSS file
.highlighted-box {
width: 100px;
height: 200px;
background-color: blue;
border: 1px solid black;
}
// JavaScript
function updateElementStyles(element: HTMLElement) {
element.classList.add('highlighted-box')
}
```
**Correct: change cssText - single reflow**
```typescript
function updateElementStyles(element: HTMLElement) {
element.style.cssText = `
width: 100px;
height: 200px;
background-color: blue;
border: 1px solid black;
`
}
```
**React example:**
```tsx
// Incorrect: changing styles one by one
function Box({ isHighlighted }: { isHighlighted: boolean }) {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (ref.current && isHighlighted) {
ref.current.style.width = '100px'
ref.current.style.height = '200px'
ref.current.style.backgroundColor = 'blue'
}
}, [isHighlighted])
return <div ref={ref}>Content</div>
}
// Correct: toggle class
function Box({ isHighlighted }: { isHighlighted: boolean }) {
return (
<div className={isHighlighted ? 'highlighted-box' : ''}>
Content
</div>
)
}
```
Prefer CSS classes over inline styles when possible. Classes are cached by the browser and provide better separation of concerns.
### 7.2 Build Index Maps for Repeated Lookups
**Impact: LOW-MEDIUM (1M ops to 2K ops)**
Multiple `.find()` calls by the same key should use a Map.
**Incorrect (O(n) per lookup):**
```typescript
function processOrders(orders: Order[], users: User[]) {
return orders.map(order => ({
...order,
user: users.find(u => u.id === order.userId)
}))
}
```
**Correct (O(1) per lookup):**
```typescript
function processOrders(orders: Order[], users: User[]) {
const userById = new Map(users.map(u => [u.id, u]))
return orders.map(order => ({
...order,
user: userById.get(order.userId)
}))
}
```
Build map once (O(n)), then all lookups are O(1).
For 1000 orders × 1000 users: 1M ops → 2K ops.
### 7.3 Cache Property Access in Loops
**Impact: LOW-MEDIUM (reduces lookups)**
Cache object property lookups in hot paths.
**Incorrect: 3 lookups × N iterations**
```typescript
for (let i = 0; i < arr.length; i++) {
process(obj.config.settings.value)
}
```
**Correct: 1 lookup total**
```typescript
const value = obj.config.settings.value
const len = arr.length
for (let i = 0; i < len; i++) {
process(value)
}
```
### 7.4 Cache Repeated Function Calls
**Impact: MEDIUM (avoid redundant computation)**
Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
**Incorrect: redundant computation**
```typescript
function ProjectList({ projects }: { projects: Project[] }) {
return (
<div>
{projects.map(project => {
// slugify() called 100+ times for same project names
const slug = slugify(project.name)
return <ProjectCard key={project.id} slug={slug} />
})}
</div>
)
}
```
**Correct: cached results**
```typescript
// Module-level cache
const slugifyCache = new Map<string, string>()
function cachedSlugify(text: string): string {
if (slugifyCache.has(text)) {
return slugifyCache.get(text)!
}
const result = slugify(text)
slugifyCache.set(text, result)
return result
}
function ProjectList({ projects }: { projects: Project[] }) {
return (
<div>
{projects.map(project => {
// Computed only once per unique project name
const slug = cachedSlugify(project.name)
return <ProjectCard key={project.id} slug={slug} />
})}
</div>
)
}
```
**Simpler pattern for single-value functions:**
```typescript
let isLoggedInCache: boolean | null = null
function isLoggedIn(): boolean {
if (isLoggedInCache !== null) {
return isLoggedInCache
}
isLoggedInCache = document.cookie.includes('auth=')
return isLoggedInCache
}
// Clear cache when auth changes
function onAuthChange() {
isLoggedInCache = null
}
```
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
Reference: [https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
### 7.5 Cache Storage API Calls
**Impact: LOW-MEDIUM (reduces expensive I/O)**
`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
**Incorrect: reads storage on every call**
```typescript
function getTheme() {
return localStorage.getItem('theme') ?? 'light'
}
// Called 10 times = 10 storage reads
```
**Correct: Map cache**
```typescript
const storageCache = new Map<string, string | null>()
function getLocalStorage(key: string) {
if (!storageCache.has(key)) {
storageCache.set(key, localStorage.getItem(key))
}
return storageCache.get(key)
}
function setLocalStorage(key: string, value: string) {
localStorage.setItem(key, value)
storageCache.set(key, value) // keep cache in sync
}
```
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
**Cookie caching:**
```typescript
let cookieCache: Record<string, string> | null = null
function getCookie(name: string) {
if (!cookieCache) {
cookieCache = Object.fromEntries(
document.cookie.split('; ').map(c => c.split('='))
)
}
return cookieCache[name]
}
```
**Important: invalidate on external changes**
```typescript
window.addEventListener('storage', (e) => {
if (e.key) storageCache.delete(e.key)
})
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
storageCache.clear()
}
})
```
If storage can change externally (another tab, server-set cookies), invalidate cache:
### 7.6 Combine Multiple Array Iterations
**Impact: LOW-MEDIUM (reduces iterations)**
Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
**Incorrect: 3 iterations**
```typescript
const admins = users.filter(u => u.isAdmin)
const testers = users.filter(u => u.isTester)
const inactive = users.filter(u => !u.isActive)
```
**Correct: 1 iteration**
```typescript
const admins: User[] = []
const testers: User[] = []
const inactive: User[] = []
for (const user of users) {
if (user.isAdmin) admins.push(user)
if (user.isTester) testers.push(user)
if (!user.isActive) inactive.push(user)
}
```
### 7.7 Early Length Check for Array Comparisons
**Impact: MEDIUM-HIGH (avoids expensive operations when lengths differ)**
When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.
In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).
**Incorrect: always runs expensive comparison**
```typescript
function hasChanges(current: string[], original: string[]) {
// Always sorts and joins, even when lengths differ
return current.sort().join() !== original.sort().join()
}
```
Two O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.
**Correct (O(1) length check first):**
```typescript
function hasChanges(current: string[], original: string[]) {
// Early return if lengths differ
if (current.length !== original.length) {
return true
}
// Only sort/join when lengths match
const currentSorted = current.toSorted()
const originalSorted = original.toSorted()
for (let i = 0; i < currentSorted.length; i++) {
if (currentSorted[i] !== originalSorted[i]) {
return true
}
}
return false
}
```
This new approach is more efficient because:
- It avoids the overhead of sorting and joining the arrays when lengths differ
- It avoids consuming memory for the joined strings (especially important for large arrays)
- It avoids mutating the original arrays
- It returns early when a difference is found
### 7.8 Early Return from Functions
**Impact: LOW-MEDIUM (avoids unnecessary computation)**
Return early when result is determined to skip unnecessary processing.
**Incorrect: processes all items even after finding answer**
```typescript
function validateUsers(users: User[]) {
let hasError = false
let errorMessage = ''
for (const user of users) {
if (!user.email) {
hasError = true
errorMessage = 'Email required'
}
if (!user.name) {
hasError = true
errorMessage = 'Name required'
}
// Continues checking all users even after error found
}
return hasError ? { valid: false, error: errorMessage } : { valid: true }
}
```
**Correct: returns immediately on first error**
```typescript
function validateUsers(users: User[]) {
for (const user of users) {
if (!user.email) {
return { valid: false, error: 'Email required' }
}
if (!user.name) {
return { valid: false, error: 'Name required' }
}
}
return { valid: true }
}
```
### 7.9 Hoist RegExp Creation
**Impact: LOW-MEDIUM (avoids recreation)**
Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
**Incorrect: new RegExp every render**
```tsx
function Highlighter({ text, query }: Props) {
const regex = new RegExp(`(${query})`, 'gi')
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
}
```
**Correct: memoize or hoist**
```tsx
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
function Highlighter({ text, query }: Props) {
const regex = useMemo(
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
[query]
)
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
}
```
**Warning: global regex has mutable state**
```typescript
const regex = /foo/g
regex.test('foo') // true, lastIndex = 3
regex.test('foo') // false, lastIndex = 0
```
Global regex (`/g`) has mutable `lastIndex` state:
### 7.10 Use Loop for Min/Max Instead of Sort
**Impact: LOW (O(n) instead of O(n log n))**
Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.
**Incorrect (O(n log n) - sort to find latest):**
```typescript
interface Project {
id: string
name: string
updatedAt: number
}
function getLatestProject(projects: Project[]) {
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
return sorted[0]
}
```
Sorts the entire array just to find the maximum value.
**Incorrect (O(n log n) - sort for oldest and newest):**
```typescript
function getOldestAndNewest(projects: Project[]) {
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
}
```
Still sorts unnecessarily when only min/max are needed.
**Correct (O(n) - single loop):**
```typescript
function getLatestProject(projects: Project[]) {
if (projects.length === 0) return null
let latest = projects[0]
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt > latest.updatedAt) {
latest = projects[i]
}
}
return latest
}
function getOldestAndNewest(projects: Project[]) {
if (projects.length === 0) return { oldest: null, newest: null }
let oldest = projects[0]
let newest = projects[0]
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
}
return { oldest, newest }
}
```
Single pass through the array, no copying, no sorting.
**Alternative: Math.min/Math.max for small arrays**
```typescript
const numbers = [5, 2, 8, 1, 9]
const min = Math.min(...numbers)
const max = Math.max(...numbers)
```
This works for small arrays but can be slower for very large arrays due to spread operator limitations. Use the loop approach for reliability.
### 7.11 Use Set/Map for O(1) Lookups
**Impact: LOW-MEDIUM (O(n) to O(1))**
Convert arrays to Set/Map for repeated membership checks.
**Incorrect (O(n) per check):**
```typescript
const allowedIds = ['a', 'b', 'c', ...]
items.filter(item => allowedIds.includes(item.id))
```
**Correct (O(1) per check):**
```typescript
const allowedIds = new Set(['a', 'b', 'c', ...])
items.filter(item => allowedIds.has(item.id))
```
### 7.12 Use toSorted() Instead of sort() for Immutability
**Impact: MEDIUM-HIGH (prevents mutation bugs in React state)**
`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.
**Incorrect: mutates original array**
```typescript
function UserList({ users }: { users: User[] }) {
// Mutates the users prop array!
const sorted = useMemo(
() => users.sort((a, b) => a.name.localeCompare(b.name)),
[users]
)
return <div>{sorted.map(renderUser)}</div>
}
```
**Correct: creates new array**
```typescript
function UserList({ users }: { users: User[] }) {
// Creates new sorted array, original unchanged
const sorted = useMemo(
() => users.toSorted((a, b) => a.name.localeCompare(b.name)),
[users]
)
return <div>{sorted.map(renderUser)}</div>
}
```
**Why this matters in React:**
1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
**Browser support: fallback for older browsers**
```typescript
// Fallback for older browsers
const sorted = [...items].sort((a, b) => a.value - b.value)
```
`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
**Other immutable array methods:**
- `.toSorted()` - immutable sort
- `.toReversed()` - immutable reverse
- `.toSpliced()` - immutable splice
- `.with()` - immutable element replacement
---
## 8. Advanced Patterns
**Impact: LOW**
Advanced patterns for specific cases that require careful implementation.
### 8.1 Store Event Handlers in Refs
**Impact: LOW (stable subscriptions)**
Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
**Incorrect: re-subscribes on every render**
```tsx
function useWindowEvent(event: string, handler: () => void) {
useEffect(() => {
window.addEventListener(event, handler)
return () => window.removeEventListener(event, handler)
}, [event, handler])
}
```
**Correct: stable subscription**
```tsx
import { useEffectEvent } from 'react'
function useWindowEvent(event: string, handler: () => void) {
const onEvent = useEffectEvent(handler)
useEffect(() => {
window.addEventListener(event, onEvent)
return () => window.removeEventListener(event, onEvent)
}, [event])
}
```
**Alternative: use `useEffectEvent` if you're on latest React:**
`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
### 8.2 useLatest for Stable Callback Refs
**Impact: LOW (prevents effect re-runs)**
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
**Implementation:**
```typescript
function useLatest<T>(value: T) {
const ref = useRef(value)
useEffect(() => {
ref.current = value
}, [value])
return ref
}
```
**Incorrect: effect re-runs on every callback change**
```tsx
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
useEffect(() => {
const timeout = setTimeout(() => onSearch(query), 300)
return () => clearTimeout(timeout)
}, [query, onSearch])
}
```
**Correct: stable effect, fresh callback**
```tsx
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
const onSearchRef = useLatest(onSearch)
useEffect(() => {
const timeout = setTimeout(() => onSearchRef.current(query), 300)
return () => clearTimeout(timeout)
}, [query])
}
```
---
## References
1. [https://react.dev](https://react.dev)
2. [https://nextjs.org](https://nextjs.org)
3. [https://swr.vercel.app](https://swr.vercel.app)
4. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
5. [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
6. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
7. [https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
================================================
FILE: .claude/skills/react-best-practices/README.md
================================================
# React Best Practices
A structured repository for creating and maintaining React Best Practices optimized for agents and LLMs.
## Structure
- `rules/` - Individual rule files (one per rule)
- `_sections.md` - Section metadata (titles, impacts, descriptions)
- `_template.md` - Template for creating new rules
- `area-description.md` - Individual rule files
- `src/` - Build scripts and utilities
- `metadata.json` - Document metadata (version, organization, abstract)
- __`AGENTS.md`__ - Compiled output (generated)
- __`test-cases.json`__ - Test cases for LLM evaluation (generated)
## Getting Started
1. Install dependencies:
```bash
pnpm install
```
2. Build AGENTS.md from rules:
```bash
pnpm build
```
3. Validate rule files:
```bash
pnpm validate
```
4. Extract test cases:
```bash
pnpm extract-tests
```
## Creating a New Rule
1. Copy `rules/_template.md` to `rules/area-description.md`
2. Choose the appropriate area prefix:
- `async-` for Eliminating Waterfalls (Section 1)
- `bundle-` for Bundle Size Optimization (Section 2)
- `server-` for Server-Side Performance (Section 3)
- `client-` for Client-Side Data Fetching (Section 4)
- `rerender-` for Re-render Optimization (Section 5)
- `rendering-` for Rendering Performance (Section 6)
- `js-` for JavaScript Performance (Section 7)
- `advanced-` for Advanced Patterns (Section 8)
3. Fill in the frontmatter and content
4. Ensure you have clear examples with explanations
5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
## Rule File Structure
Each rule file should follow this structure:
```markdown
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description
tags: tag1, tag2, tag3
---
## Rule Title Here
Brief explanation of the rule and why it matters.
**Incorrect (description of what's wrong):**
```typescript
// Bad code example
```
**Correct (description of what's right):**
```typescript
// Good code example
```
Optional explanatory text after examples.
Reference: [Link](https://example.com)
## File Naming Convention
- Files starting with `_` are special (excluded from build)
- Rule files: `area-description.md` (e.g., `async-parallel.md`)
- Section is automatically inferred from filename prefix
- Rules are sorted alphabetically by title within each section
- IDs (e.g., 1.1, 1.2) are auto-generated during build
## Impact Levels
- `CRITICAL` - Highest priority, major performance gains
- `HIGH` - Significant performance improvements
- `MEDIUM-HIGH` - Moderate-high gains
- `MEDIUM` - Moderate performance improvements
- `LOW-MEDIUM` - Low-medium gains
- `LOW` - Incremental improvements
## Scripts
- `pnpm build` - Compile rules into AGENTS.md
- `pnpm validate` - Validate all rule files
- `pnpm extract-tests` - Extract test cases for LLM evaluation
- `pnpm dev` - Build and validate
## Contributing
When adding or modifying rules:
1. Use the correct filename prefix for your section
2. Follow the `_template.md` structure
3. Include clear bad/good examples with explanations
4. Add appropriate tags
5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
6. Rules are automatically sorted by title - no need to manage numbers!
## Acknowledgments
Originally created by [@shuding](https://x.com/shuding) at [Vercel](https://vercel.com).
================================================
FILE: .claude/skills/react-best-practices/SKILL.md
================================================
---
name: vercel-react-best-practices
description: React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
---
# Vercel React Best Practices
Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 45 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
## When to Apply
Reference these guidelines when:
- Writing new React components or Next.js pages
- Implementing data fetching (client or server-side)
- Reviewing code for performance issues
- Refactoring existing React/Next.js code
- Optimizing bundle size or load times
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
| 3 | Server-Side Performance | HIGH | `server-` |
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
| 6 | Rendering Performance | MEDIUM | `rendering-` |
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
| 8 | Advanced Patterns | LOW | `advanced-` |
## Quick Reference
### 1. Eliminating Waterfalls (CRITICAL)
- `async-defer-await` - Move await into branches where actually used
- `async-parallel` - Use Promise.all() for independent operations
- `async-dependencies` - Use better-all for partial dependencies
- `async-api-routes` - Start promises early, await late in API routes
- `async-suspense-boundaries` - Use Suspense to stream content
### 2. Bundle Size Optimization (CRITICAL)
- `bundle-barrel-imports` - Import directly, avoid barrel files
- `bundle-dynamic-imports` - Use next/dynamic for heavy components
- `bundle-defer-third-party` - Load analytics/logging after hydration
- `bundle-conditional` - Load modules only when feature is activated
- `bundle-preload` - Preload on hover/focus for perceived speed
### 3. Server-Side Performance (HIGH)
- `server-cache-react` - Use React.cache() for per-request deduplication
- `server-cache-lru` - Use LRU cache for cross-request caching
- `server-serialization` - Minimize data passed to client components
- `server-parallel-fetching` - Restructure components to parallelize fetches
- `server-after-nonblocking` - Use after() for non-blocking operations
### 4. Client-Side Data Fetching (MEDIUM-HIGH)
- `client-swr-dedup` - Use SWR for automatic request deduplication
- `client-event-listeners` - Deduplicate global event listeners
### 5. Re-render Optimization (MEDIUM)
- `rerender-defer-reads` - Don't subscribe to state only used in callbacks
- `rerender-memo` - Extract expensive work into memoized components
- `rerender-dependencies` - Use primitive dependencies in effects
- `rerender-derived-state` - Subscribe to derived booleans, not raw values
- `rerender-functional-setstate` - Use functional setState for stable callbacks
- `rerender-lazy-state-init` - Pass function to useState for expensive values
- `rerender-transitions` - Use startTransition for non-urgent updates
### 6. Rendering Performance (MEDIUM)
- `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element
- `rendering-content-visibility` - Use content-visibility for long lists
- `rendering-hoist-jsx` - Extract static JSX outside components
- `rendering-svg-precision` - Reduce SVG coordinate precision
- `rendering-hydration-no-flicker` - Use inline script for client-only data
- `rendering-activity` - Use Activity component for show/hide
- `rendering-conditional-render` - Use ternary, not && for conditionals
### 7. JavaScript Performance (LOW-MEDIUM)
- `js-batch-dom-css` - Group CSS changes via classes or cssText
- `js-index-maps` - Build Map for repeated lookups
- `js-cache-property-access` - Cache object properties in loops
- `js-cache-function-results` - Cache function results in module-level Map
- `js-cache-storage` - Cache localStorage/sessionStorage reads
- `js-combine-iterations` - Combine multiple filter/map into one loop
- `js-length-check-first` - Check array length before expensive comparison
- `js-early-exit` - Return early from functions
- `js-hoist-regexp` - Hoist RegExp creation outside loops
- `js-min-max-loop` - Use loop for min/max instead of sort
- `js-set-map-lookups` - Use Set/Map for O(1) lookups
- `js-tosorted-immutable` - Use toSorted() for immutability
### 8. Advanced Patterns (LOW)
- `advanced-event-handler-refs` - Store event handlers in refs
- `advanced-use-latest` - useLatest for stable callback refs
## How to Use
Read individual rule files for detailed explanations and code examples:
```
rules/async-parallel.md
rules/bundle-barrel-imports.md
rules/_sections.md
```
Each rule file contains:
- Brief explanation of why it matters
- Incorrect code example with explanation
- Correct code example with explanation
- Additional context and references
## Full Compiled Document
For the complete guide with all rules expanded: `AGENTS.md`
================================================
FILE: .claude/skills/react-best-practices/metadata.json
================================================
{
"version": "0.1.0",
"organization": "Vercel Engineering",
"date": "January 2026",
"abstract": "Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.",
"references": [
"https://react.dev",
"https://nextjs.org",
"https://swr.vercel.app",
"https://github.com/shuding/better-all",
"https://github.com/isaacs/node-lru-cache",
"https://vercel.com/blog/how-we-optimized-package-imports-in-next-js",
"https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast"
]
}
================================================
FILE: .claude/skills/react-best-practices/rules/_sections.md
================================================
# Sections
This file defines all sections, their ordering, impact levels, and descriptions.
The section ID (in parentheses) is the filename prefix used to group rules.
---
## 1. Eliminating Waterfalls (async)
**Impact:** CRITICAL
**Description:** Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
## 2. Bundle Size Optimization (bundle)
**Impact:** CRITICAL
**Description:** Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
## 3. Server-Side Performance (server)
**Impact:** HIGH
**Description:** Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
## 4. Client-Side Data Fetching (client)
**Impact:** MEDIUM-HIGH
**Description:** Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
## 5. Re-render Optimization (rerender)
**Impact:** MEDIUM
**Description:** Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
## 6. Rendering Performance (rendering)
**Impact:** MEDIUM
**Description:** Optimizing the rendering process reduces the work the browser needs to do.
## 7. JavaScript Performance (js)
**Impact:** LOW-MEDIUM
**Description:** Micro-optimizations for hot paths can add up to meaningful improvements.
## 8. Advanced Patterns (advanced)
**Impact:** LOW
**Description:** Advanced patterns for specific cases that require careful implementation.
================================================
FILE: .claude/skills/react-best-practices/rules/_template.md
================================================
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description of impact (e.g., "20-50% improvement")
tags: tag1, tag2
---
## Rule Title Here
**Impact: MEDIUM (optional impact description)**
Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.
**Incorrect (description of what's wrong):**
```typescript
// Bad code example here
const bad = example()
```
**Correct (description of what's right):**
```typescript
// Good code example here
const good = example()
```
Reference: [Link to documentation or resource](https://example.com)
================================================
FILE: .claude/skills/react-best-practices/rules/advanced-event-handler-refs.md
================================================
---
title: Store Event Handlers in Refs
impact: LOW
impactDescription: stable subscriptions
tags: advanced, hooks, refs, event-handlers, optimization
---
## Store Event Handlers in Refs
Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
**Incorrect (re-subscribes on every render):**
```tsx
function useWindowEvent(event: string, handler: () => void) {
useEffect(() => {
window.addEventListener(event, handler)
return () => window.removeEventListener(event, handler)
}, [event, handler])
}
```
**Correct (stable subscription):**
```tsx
function useWindowEvent(event: string, handler: () => void) {
const handlerRef = useRef(handler)
useEffect(() => {
handlerRef.current = handler
}, [handler])
useEffect(() => {
const listener = () => handlerRef.current()
window.addEventListener(event, listener)
return () => window.removeEventListener(event, listener)
}, [event])
}
```
**Alternative: use `useEffectEvent` if you're on latest React:**
```tsx
import { useEffectEvent } from 'react'
function useWindowEvent(event: string, handler: () => void) {
const onEvent = useEffectEvent(handler)
useEffect(() => {
window.addEventListener(event, onEvent)
return () => window.removeEventListener(event, onEvent)
}, [event])
}
```
`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
================================================
FILE: .claude/skills/react-best-practices/rules/advanced-use-latest.md
================================================
---
title: useLatest for Stable Callback Refs
impact: LOW
impactDescription: prevents effect re-runs
tags: advanced, hooks, useLatest, refs, optimization
---
## useLatest for Stable Callback Refs
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
**Implementation:**
```typescript
function useLatest<T>(value: T) {
const ref = useRef(value)
useEffect(() => {
ref.current = value
}, [value])
return ref
}
```
**Incorrect (effect re-runs on every callback change):**
```tsx
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
useEffect(() => {
const timeout = setTimeout(() => onSearch(query), 300)
return () => clearTimeout(timeout)
}, [query, onSearch])
}
```
**Correct (stable effect, fresh callback):**
```tsx
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
const onSearchRef = useLatest(onSearch)
useEffect(() => {
const timeout = setTimeout(() => onSearchRef.current(query), 300)
return () => clearTimeout(timeout)
}, [query])
}
```
================================================
FILE: .claude/skills/react-best-practices/rules/async-api-routes.md
================================================
---
title: Prevent Waterfall Chains in API Routes
impact: CRITICAL
impactDescription: 2-10× improvement
tags: api-routes, server-actions, waterfalls, parallelization
---
## Prevent Waterfall Chains in API Routes
In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
**Incorrect (config waits for auth, data waits for both):**
```typescript
export async function GET(request: Request) {
const session = await auth()
const config = await fetchConfig()
const data = await fetchData(session.user.id)
return Response.json({ data, config })
}
```
**Correct (auth and config start immediately):**
```typescript
export async function GET(request: Request) {
const sessionPromise = auth()
const configPromise = fetchConfig()
const session = await sessionPromise
const [config, data] = await Promise.all([
configPromise,
fetchData(session.user.id)
])
return Response.json({ data, config })
}
```
For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
================================================
FILE: .claude/skills/react-best-practices/rules/async-defer-await.md
================================================
---
title: Defer Await Until Needed
impact: HIGH
impactDescription: avoids blocking unused code paths
tags: async, await, conditional, optimization
---
## Defer Await Until Needed
Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
**Incorrect (blocks both branches):**
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId)
if (skipProcessing) {
// Returns immediately but still waited for userData
return { skipped: true }
}
// Only this branch uses userData
return processUserData(userData)
}
```
**Correct (only blocks when needed):**
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) {
// Returns immediately without waiting
return { skipped: true }
}
// Fetch only when needed
const userData = await fetchUserData(userId)
return processUserData(userData)
}
```
**Another example (early return optimization):**
```typescript
// Incorrect: always fetches permissions
async function updateResource(resourceId: string, userId: string) {
const permissions = await fetchPermissions(userId)
const resource = await getResource(resourceId)
if (!resource) {
return { error: 'Not found' }
}
if (!permissions.canEdit) {
return { error: 'Forbidden' }
}
return await updateResourceData(resource, permissions)
}
// Correct: fetches only when needed
async function updateResource(resourceId: string, userId: string) {
const resource = await getResource(resourceId)
if (!resource) {
return { error: 'Not found' }
}
const permissions = await fetchPermissions(userId)
if (!permissions.canEdit) {
return { error: 'Forbidden' }
}
return await updateResourceData(resource, permissions)
}
```
This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
================================================
FILE: .claude/skills/react-best-practices/rules/async-dependencies.md
================================================
---
title: Dependency-Based Parallelization
impact: CRITICAL
impactDescription: 2-10× improvement
tags: async, parallelization, dependencies, better-all
---
## Dependency-Based Parallelization
For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
**Incorrect (profile waits for config unnecessarily):**
```typescript
const [user, config] = await Promise.all([
fetchUser(),
fetchConfig()
])
const profile = await fetchProfile(user.id)
```
**Correct (config and profile run in parallel):**
```typescript
import { all } from 'better-all'
const { user, config, profile } = await all({
async user() { return fetchUser() },
async config() { return fetchConfig() },
async profile() {
return fetchProfile((await this.$.user).id)
}
})
```
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
================================================
FILE: .claude/skills/react-best-practices/rules/async-parallel.md
================================================
---
title: Promise.all() for Independent Operations
impact: CRITICAL
impactDescription: 2-10× improvement
tags: async, parallelization, promises, waterfalls
---
## Promise.all() for Independent Operations
When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
**Incorrect (sequential execution, 3 round trips):**
```typescript
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
```
**Correct (parallel execution, 1 round trip):**
```typescript
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
])
```
================================================
FILE: .claude/skills/react-best-practices/rules/async-suspense-boundaries.md
================================================
---
title: Strategic Suspense Boundaries
impact: HIGH
impactDescription: faster initial paint
tags: async, suspense, streaming, layout-shift
---
## Strategic Suspense Boundaries
Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
**Incorrect (wrapper blocked by data fetching):**
```tsx
async function Page() {
const data = await fetchData() // Blocks entire page
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<div>
<DataDisplay data={data} />
</div>
<div>Footer</div>
</div>
)
}
```
The entire layout waits for data even though only the middle section needs it.
**Correct (wrapper shows immediately, data streams in):**
```tsx
function Page() {
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<div>
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>
</div>
<div>Footer</div>
</div>
)
}
async function DataDisplay() {
const data = await fetchData() // Only blocks this component
return <div>{data.content}</div>
}
```
Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
**Alternative (share promise across components):**
```tsx
function Page() {
// Start fetch immediately, but don't await
const dataPromise = fetchData()
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<Suspense fallback={<Skeleton />}>
<DataDisplay dataPromise={dataPromise} />
<DataSummary dataPromise={dataPromise} />
</Suspense>
<div>Footer</div>
</div>
)
}
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // Unwraps the promise
return <div>{data.content}</div>
}
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // Reuses the same promise
return <div>{data.summary}</div>
}
```
Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
**When NOT to use this pattern:**
- Critical data needed for layout decisions (affects positioning)
- SEO-critical content above the fold
- Small, fast queries where suspense overhead isn't worth it
- When you want to avoid layout shift (loading → content jump)
**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
================================================
FILE: .claude/skills/react-best-practices/rules/bundle-barrel-imports.md
================================================
---
title: Avoid Barrel File Imports
impact: CRITICAL
impactDescription: 200-800ms import cost, slow builds
tags: bundle, imports, tree-shaking, barrel-files, performance
---
## Avoid Barrel File Imports
Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.
**Incorrect (imports entire library):**
```tsx
import { Check, X, Menu } from 'lucide-react'
// Loads 1,583 modules, takes ~2.8s extra in dev
// Runtime cost: 200-800ms on every cold start
import { Button, TextField } from '@mui/material'
// Loads 2,225 modules, takes ~4.2s extra in dev
```
**Correct (imports only what you need):**
```tsx
import Check from 'lucide-react/dist/esm/icons/check'
import X from 'lucide-react/dist/esm/icons/x'
import Menu from 'lucide-react/dist/esm/icons/menu'
// Loads only 3 modules (~2KB vs ~1MB)
import Button from '@mui/material/Button'
import TextField from '@mui/material/TextField'
// Loads only what you use
```
**Alternative (Next.js 13.5+):**
```js
// next.config.js - use optimizePackageImports
module.exports = {
experimental: {
optimizePackageImports: ['lucide-react', '@mui/material']
}
}
// Then you can keep the ergonomic barrel imports:
import { Check, X, Menu } from 'lucide-react'
// Automatically transformed to direct imports at build time
```
Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.
Reference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
================================================
FILE: .claude/skills/react-best-practices/rules/bundle-conditional.md
================================================
---
title: Conditional Module Loading
impact: HIGH
impactDescription: loads large data only when needed
tags: bundle, conditional-loading, lazy-loading
---
## Conditional Module Loading
Load large data or modules only when a feature is activated.
**Example (lazy-load animation frames):**
```tsx
function AnimationPlayer({ enabled }: { enabled: boolean }) {
const [frames, setFrames] = useState<Frame[] | null>(null)
useEffect(() => {
if (enabled && !frames && typeof window !== 'undefined') {
import('./animation-frames.js')
.then(mod => setFrames(mod.frames))
.catch(() => setEnabled(false))
}
}, [enabled, frames])
if (!frames) return <Skeleton />
return <Canvas frames={frames} />
}
```
The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
================================================
FILE: .claude/skills/react-best-practices/rules/bundle-defer-third-party.md
================================================
---
title: Defer Non-Critical Third-Party Libraries
impact: MEDIUM
impactDescription: loads after hydration
tags: bundle, third-party, analytics, defer
---
## Defer Non-Critical Third-Party Libraries
Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
**Incorrect (blocks initial bundle):**
```tsx
import { Analytics } from '@vercel/analytics/react'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
)
}
```
**Correct (loads after hydration):**
```tsx
import dynamic from 'next/dynamic'
const Analytics = dynamic(
() => import('@vercel/analytics/react').then(m => m.Analytics),
{ ssr: false }
)
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
)
}
```
================================================
FILE: .claude/skills/react-best-practices/rules/bundle-dynamic-imports.md
================================================
---
title: Dynamic Imports for Heavy Components
impact: CRITICAL
impactDescription: directly affects TTI and LCP
tags: bundle, dynamic-import, code-splitting, next-dynamic
---
## Dynamic Imports for Heavy Components
Use `next/dynamic` to lazy-load large components not needed on initial render.
**Incorrect (Monaco bundles with main chunk ~300KB):**
```tsx
import { MonacoEditor } from './monaco-editor'
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />
}
```
**Correct (Monaco loads on demand):**
```tsx
import dynamic from 'next/dynamic'
const MonacoEditor = dynamic(
() => import('./monaco-editor').then(m => m.MonacoEditor),
{ ssr: false }
)
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />
}
```
================================================
FILE: .claude/skills/react-best-practices/rules/bundle-preload.md
================================================
---
title: Preload Based on User Intent
impact: MEDIUM
impactDescription: reduces perceived latency
tags: bundle, preload, user-intent, hover
---
## Preload Based on User Intent
Preload heavy bundles before they're needed to reduce perceived latency.
**Example (preload on hover/focus):**
```tsx
function EditorButton({ onClick }: { onClick: () => void }) {
const preload = () => {
if (typeof window !== 'undefined') {
void import('./monaco-editor')
}
}
return (
<button
onMouseEnter={preload}
onFocus={preload}
onClick={onClick}
>
Open Editor
</button>
)
}
```
**Example (preload when feature flag is enabled):**
```tsx
function FlagsProvider({ children, flags }: Props) {
useEffect(() => {
if (flags.editorEnabled && typeof window !== 'undefined') {
void import('./monaco-editor').then(mod => mod.init())
}
}, [flags.editorEnabled])
return <FlagsContext.Provider value={flags}>
{children}
</FlagsContext.Provider>
}
```
The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
================================================
FILE: .claude/skills/react-best-practices/rules/client-event-listeners.md
================================================
---
title: Deduplicate Global Event Listeners
impact: LOW
impactDescription: single listener for N components
tags: client, swr, event-listeners, subscription
---
## Deduplicate Global Event Listeners
Use `useSWRSubscription()` to share global event listeners across component instances.
**Incorrect (N instances = N listeners):**
```tsx
function useKeyboardShortcut(key: string, callback: () => void) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && e.key === key) {
callback()
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [key, callback])
}
```
When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
**Correct (N instances = 1 listener):**
```tsx
import useSWRSubscription from 'swr/subscription'
// Module-level Map to track callbacks per key
const keyCallbacks = new Map<string, Set<() => void>>()
function useKeyboardShortcut(key: string, callback: () => void) {
// Register this callback in the Map
useEffect(() => {
if (!keyCallbacks.has(key)) {
keyCallbacks.set(key, new Set())
}
keyCallbacks.get(key)!.add(callback)
return () => {
const set = keyCallbacks.get(key)
if (set) {
set.delete(callback)
if (set.size === 0) {
keyCallbacks.delete(key)
}
}
}
}, [key, callback])
useSWRSubscription('global-keydown', () => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && keyCallbacks.has(e.key)) {
keyCallbacks.get(e.key)!.forEach(cb => cb())
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
})
}
function Profile() {
// Multiple shortcuts will share the same listener
useKeyboardShortcut('p', () => { /* ... */ })
useKeyboardShortcut('k', () => { /* ... */ })
// ...
}
```
================================================
FILE: .claude/skills/react-best-practices/rules/client-swr-dedup.md
================================================
---
title: Use SWR for Automatic Deduplication
impact: MEDIUM-HIGH
impactDescription: automatic deduplication
tags: client, swr, deduplication, data-fetching
---
## Use SWR for Automatic Deduplication
SWR enables request deduplication, caching, and revalidation across component instances.
**Incorrect (no deduplication, each instance fetches):**
```tsx
function UserList() {
const [users, setUsers] = useState([])
useEffect(() => {
fetch('/api/users')
.then(r => r.json())
.then(setUsers)
}, [])
}
```
**Correct (multiple instances share one request):**
```tsx
import useSWR from 'swr'
function UserList() {
const { data: users } = useSWR('/api/users', fetcher)
}
```
**For immutable data:**
```tsx
import { useImmutableSWR } from '@/lib/swr'
function StaticContent() {
const { data } = useImmutableSWR('/api/config', fetcher)
}
```
**For mutations:**
```tsx
import { useSWRMutation } from 'swr/mutation'
function UpdateButton() {
const { trigger } = useSWRMutation('/api/user', updateUser)
return <button onClick={() => trigger()}>Update</button>
}
```
Reference: [https://swr.vercel.app](https://swr.vercel.app)
================================================
FILE: .claude/skills/react-best-practices/rules/js-batch-dom-css.md
================================================
---
title: Batch DOM CSS Changes
impact: MEDIUM
impactDescription: reduces reflows/repaints
tags: javascript, dom, css, performance, reflow
---
## Batch DOM CSS Changes
Avoid changing styles one property at a time. Group multiple CSS changes together via classes or `cssText` to minimize browser reflows.
**Incorrect (multiple reflows):**
```typescript
function updateElementStyles(element: HTMLElement) {
// Each line triggers a reflow
element.style.width = '100px'
element.style.height = '200px'
element.style.backgroundColor = 'blue'
element.style.border = '1px solid black'
}
```
**Correct (add class - single reflow):**
```typescript
// CSS file
.highlighted-box {
width: 100px;
height: 200px;
background-color: blue;
border: 1px solid black;
}
// JavaScript
function updateElementStyles(element: HTMLElement) {
element.classList.add('highlighted-box')
}
```
**Correct (change cssText - single reflow):**
```typescript
function updateElementStyles(element: HTMLElement) {
element.style.cssText = `
width: 100px;
height: 200px;
background-color: blue;
border: 1px solid black;
`
}
```
**React example:**
```tsx
// Incorrect: changing styles one by one
function Box({ isHighlighted }: { isHighlighted: boolean }) {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (ref.current && isHighlighted) {
ref.current.style.width = '100px'
ref.current.style.height = '200px'
ref.current.style.backgroundColor = 'blue'
}
}, [isHighlighted])
return <div ref={ref}>Content</div>
}
// Correct: toggle class
function Box({ isHighlighted }: { isHighlighted: boolean }) {
return (
<div className={isHighlighted ? 'highlighted-box' : ''}>
Content
</div>
)
}
```
Prefer CSS classes over inline styles when possible. Classes are cached by the browser and provide better separation of concerns.
================================================
FILE: .claude/skills/react-best-practices/rules/js-cache-function-results.md
================================================
---
title: Cache Repeated Function Calls
impact: MEDIUM
impactDescription: avoid redundant computation
tags: javascript, cache, memoization, performance
---
## Cache Repeated Function Calls
Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
**Incorrect (redundant computation):**
```typescript
function ProjectList({ projects }: { projects: Project[] }) {
return (
<div>
{projects.map(project => {
// slugify() called 100+ times for same project names
const slug = slugify(project.name)
return <ProjectCard key={project.id} slug={slug} />
})}
</div>
)
}
```
**Correct (cached results):**
```typescript
// Module-level cache
const slugifyCache = new Map<string, string>()
function cachedSlugify(text: string): string {
if (slugifyCache.has(text)) {
return slugifyCache.get(text)!
}
const result = slugify(text)
slugifyCache.set(text, result)
return result
}
function ProjectList({ projects }: { projects: Project[] }) {
return (
<div>
{projects.map(project => {
// Computed only once per unique project name
const slug = cachedSlugify(project.name)
return <ProjectCard key={project.id} slug={slug} />
})}
</div>
)
}
```
**Simpler pattern for single-value functions:**
```typescript
let isLoggedInCache: boolean | null = null
function isLoggedIn(): boolean {
if (isLoggedInCache !== null) {
return isLoggedInCache
}
isLoggedInCache = document.cookie.includes('auth=')
return isLoggedInCache
}
// Clear cache when auth changes
function onAuthChange() {
isLoggedInCache = null
}
```
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
Reference: [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
================================================
FILE: .claude/skills/react-best-practices/rules/js-cache-property-access.md
================================================
---
title: Cache Property Access in Loops
impact: LOW-MEDIUM
impactDescription: reduces lookups
tags: javascript, loops, optimization, caching
---
## Cache Property Access in Loops
Cache object property lookups in hot paths.
**Incorrect (3 lookups × N iterations):**
```typescript
for (let i = 0; i < arr.length; i++) {
process(obj.config.settings.value)
}
```
**Correct (1 lookup total):**
```typescript
const value = obj.config.settings.value
const len = arr.length
for (let i = 0; i < len; i++) {
process(value)
}
```
================================================
FILE: .claude/skills/react-best-practices/rules/js-cache-storage.md
================================================
---
title: Cache Storage API Calls
impact: LOW-MEDIUM
impactDescription: reduces expensive I/O
tags: javascript, localStorage, storage, caching, performance
---
## Cache Storage API Calls
`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
**Incorrect (reads storage on every call):**
```typescript
function getTheme() {
return localStorage.getItem('theme') ?? 'light'
}
// Called 10 times = 10 storage reads
```
**Correct (Map cache):**
```typescript
const storageCache = new Map<string, string | null>()
function getLocalStorage(key: string) {
if (!storageCache.has(key)) {
storageCache.set(key, localStorage.getItem(key))
}
return storageCache.get(key)
}
function setLocalStorage(key: string, value: string) {
localStorage.setItem(key, value)
storageCache.set(key, value) // keep cache in sync
}
```
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
**Cookie caching:**
```typescript
let cookieCache: Record<string, string> | null = null
function getCookie(name: string) {
if (!cookieCache) {
cookieCache = Object.fromEntries(
document.cookie.split('; ').map(c => c.split('='))
)
}
return cookieCache[name]
}
```
**Important (invalidate on external changes):**
If storage can change externally (another tab, server-set cookies), invalidate cache:
```typescript
window.addEventListener('storage', (e) => {
if (e.key) storageCache.delete(e.key)
})
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
storageCache.clear()
}
})
```
================================================
FILE: .claude/skills/react-best-practices/rules/js-combine-iterations.md
================================================
---
title: Combine Multiple Array Iterations
impact: LOW-MEDIUM
impactDescription: reduces iterations
tags: javascript, arrays, loops, performance
---
## Combine Multiple Array Iterations
Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
**Incorrect (3 iterations):**
```typescript
const admins = users.filter(u => u.isAdmin)
const testers = users.filter(u => u.isTester)
const inactive = users.filter(u => !u.isActive)
```
**Correct (1 iteration):**
```typescript
const admins: User[] = []
const testers: User[] = []
const inactive: User[] = []
for (const user of users) {
if (user.isAdmin) admins.push(user)
if (user.isTester) testers.push(user)
if (!user.isActive) inactive.push(user)
}
```
================================================
FILE: .claude/skills/react-best-practices/rules/js-early-exit.md
================================================
---
title: Early Return from Functions
impact: LOW-MEDIUM
impactDescription: avoids unnecessary computation
tags: javascript, functions, optimization, early-return
---
## Early Return from Functions
Return early when result is determined to skip unnecessary processing.
**Incorrect (processes all items even after finding answer):**
```typescript
function validateUsers(users: User[]) {
let hasError = false
let errorMessage = ''
for (const user of users) {
if (!user.email) {
hasError = true
errorMessage = 'Email required'
}
if (!user.name) {
hasError = true
errorMessage = 'Name required'
}
// Continues checking all users even after error found
}
return hasError ? { valid: false, error: errorMessage } : { valid: true }
}
```
**Correct (returns immediately on first error):**
```typescript
function validateUsers(users: User[]) {
for (const user of users) {
if (!user.email) {
return { valid: false, error: 'Email required' }
}
if (!user.name) {
return { valid: false, error: 'Name required' }
}
}
return { valid: true }
}
```
================================================
FILE: .claude/skills/react-best-practices/rules/js-hoist-regexp.md
================================================
---
title: Hoist RegExp Creation
impact: LOW-MEDIUM
impactDescription: avoids recreation
tags: javascript, regexp, optimization, memoization
---
## Hoist RegExp Creation
Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
**Incorrect (new RegExp every render):**
```tsx
function Highlighter({ text, query }: Props) {
const regex = new RegExp(`(${query})`, 'gi')
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
}
```
**Correct (memoize or hoist):**
```tsx
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
function Highlighter({ text, query }: Props) {
const regex = useMemo(
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
[query]
)
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
}
```
**Warning (global regex has mutable state):**
Global regex (`/g`) has mutable `lastIndex` state:
```typescript
const regex = /foo/g
regex.test('foo') // true, lastIndex = 3
regex.test('foo') // false, lastIndex = 0
```
================================================
FILE: .claude/skills/react-best-practices/rules/js-index-maps.md
================================================
---
title: Build Index Maps for Repeated Lookups
impact: LOW-MEDIUM
impactDescription: 1M ops to 2K ops
tags: javascript, map, indexing, optimization, performance
---
## Build Index Maps for Repeated Lookups
Multiple `.find()` calls by the same key should use a Map.
**Incorrect (O(n) per lookup):**
```typescript
function processOrders(orders: Order[], users: User[]) {
return orders.map(order => ({
...order,
user: users.find(u => u.id === order.userId)
}))
}
```
**Correct (O(1) per lookup):**
```typescript
function processOrders(orders: Order[], users: User[]) {
const userById = new Map(users.map(u => [u.id, u]))
return orders.map(order => ({
...order,
user: userById.get(order.userId)
}))
}
```
Build map once (O(n)), then all lookups are O(1).
For 1000 orders × 1000 users: 1M ops → 2K ops.
================================================
FILE: .claude/skills/react-best-practices/rules/js-length-check-first.md
================================================
---
title: Early Length Check for Array Comparisons
impact: MEDIUM-HIGH
impactDescription: avoids expensive operations when lengths differ
tags: javascript, arrays, performance, optimization, comparison
---
## Early Length Check for Array Comparisons
When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.
In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).
**Incorrect (always runs expensive comparison):**
```typescript
function hasChanges(current: string[], original: string[]) {
// Always sorts and joins, even when lengths differ
return current.sort().join() !== original.sort().join()
}
```
Two O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.
**Correct (O(1) length check first):**
```typescript
function hasChanges(current: string[], original: string[]) {
// Early return if lengths differ
if (current.length !== original.length) {
return true
}
// Only sort/join when lengths match
const currentSorted = current.toSorted()
const originalSorted = original.toSorted()
for (let i = 0; i < currentSorted.length; i++) {
if (currentSorted[i] !== originalSorted[i]) {
return true
}
}
return false
}
```
This new approach is more efficient because:
- It avoids the overhead of sorting and joining the arrays when lengths differ
- It avoids consuming memory for the joined strings (especially important for large arrays)
- It avoids mutating the original arrays
- It returns early when a difference is found
================================================
FILE: .claude/skills/react-best-practices/rules/js-min-max-loop.md
================================================
---
title: Use Loop for Min/Max Instead of Sort
impact: LOW
impactDescription: O(n) instead of O(n log n)
tags: javascript, arrays, performance, sorting, algorithms
---
## Use Loop for Min/Max Instead of Sort
Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.
**Incorrect (O(n log n) - sort to find latest):**
```typescript
interface Project {
id: string
name: string
updatedAt: number
}
function getLatestProject(projects: Project[]) {
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
return sorted[0]
}
```
Sorts the entire array just to find the maximum value.
**Incorrect (O(n log n) - sort for oldest and newest):**
```typescript
function getOldestAndNewest(projects: Project[]) {
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
}
```
Still sorts unnecessarily when only min/max are needed.
**Correct (O(n) - single loop):**
```typescript
function getLatestProject(projects: Project[]) {
if (projects.length === 0) return null
let latest = projects[0]
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt > latest.updatedAt) {
latest = projects[i]
}
}
return latest
}
function getOldestAndNewest(projects: Project[]) {
if (projects.length === 0) return { oldest: null, newest: null }
let oldest = projects[0]
let newest = projects[0]
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
}
return { oldest, newest }
}
```
Single pass through the array, no copying, no sorting.
**Alternative (Math.min/Math.max for small arrays):**
```typescript
const numbers = [5, 2, 8, 1, 9]
const min = Math.min(...numbers)
const max = Math.max(...numbers)
```
This works for small arrays but can be slower for very large arrays due to spread operator limitations. Use the loop approach for reliability.
================================================
FILE: .claude/skills/react-best-practices/rules/js-set-map-lookups.md
================================================
---
title: Use Set/Map for O(1) Lookups
impact: LOW-MEDIUM
impactDescription: O(n) to O(1)
tags: javascript, set, map, data-structures, performance
---
## Use Set/Map for O(1) Lookups
Convert arrays to Set/Map for repeated membership checks.
**Incorrect (O(n) per check):**
```typescript
const allowedIds = ['a', 'b', 'c', ...]
items.filter(item => allowedIds.includes(item.id))
```
**Correct (O(1) per check):**
```typescript
const allowedIds = new Set(['a', 'b', 'c', ...])
items.filter(item => allowedIds.has(item.id))
```
================================================
FILE: .claude/skills/react-best-practices/rules/js-tosorted-immutable.md
================================================
---
title: Use toSorted() Instead of sort() for Immutability
impact: MEDIUM-HIGH
impactDescription: prevents mutation bugs in React state
tags: javascript, arrays, immutability, react, state, mutation
---
## Use toSorted() Instead of sort() for Immutability
`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.
**Incorrect (mutates original array):**
```typescript
function UserList({ users }: { users: User[] }) {
// Mutates the users prop array!
const sorted = useMemo(
() => users.sort((a, b) => a.name.localeCompare(b.name)),
[users]
)
return <div>{sorted.map(renderUser)}</div>
}
```
**Correct (creates new array):**
```typescript
function UserList({ users }: { users: User[] }) {
// Creates new sorted array, original unchanged
const sorted = useMemo(
() => users.toSorted((a, b) => a.name.localeCompare(b.name)),
[users]
)
return <div>{sorted.map(renderUser)}</div>
}
```
**Why this matters in React:**
1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
**Browser support (fallback for older browsers):**
`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
```typescript
// Fallback for older browsers
const sorted = [...items].sort((a, b) => a.value - b.value)
```
**Other immutable array methods:**
- `.toSorted()` - immutable sort
- `.toReversed()` - immutable reverse
- `.toSpliced()` - immutable splice
- `.with()` - immutable element replacement
================================================
FILE: .claude/skills/react-best-practices/rules/rendering-activity.md
================================================
---
title: Use Activity Component for Show/Hide
impact: MEDIUM
impactDescription: preserves state/DOM
tags: rendering, activity, visibility, state-preservation
---
## Use Activity Component for Show/Hide
Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.
**Usage:**
```tsx
import { Activity } from 'react'
function Dropdown({ isOpen }: Props) {
return (
<Activity mode={isOpen ? 'visible' : 'hidden'}>
<ExpensiveMenu />
</Activity>
)
}
```
Avoids expensive re-renders and state loss.
================================================
FILE: .claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md
================================================
---
title: Animate SVG Wrapper Instead of SVG Element
impact: LOW
impactDescription: enables hardware acceleration
tags: rendering, svg, css, animation, performance
---
## Animate SVG Wrapper Instead of SVG Element
Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.
**Incorrect (animating SVG directly - no hardware acceleration):**
```tsx
function LoadingSpinner() {
return (
<svg
className="animate-spin"
width="24"
height="24"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
)
}
```
**Correct (animating wrapper div - hardware accelerated):**
```tsx
function LoadingSpinner() {
return (
<div className="animate-spin">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
</div>
)
}
```
This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
================================================
FILE: .claude/skills/react-best-practices/rules/rendering-conditional-render.md
================================================
---
title: Use Explicit Conditional Rendering
impact: LOW
impactDescription: prevents rendering 0 or NaN
tags: rendering, conditional, jsx, falsy-values
---
## Use Explicit Conditional Rendering
Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
**Incorrect (renders "0" when count is 0):**
```tsx
function Badge({ count }: { count: number }) {
return (
<div>
{count && <span className="badge">{count}</span>}
</div>
)
}
// When count = 0, renders: <div>0</div>
// When count = 5, renders: <div><span class="badge">5</span></div>
```
**Correct (renders nothing when count is 0):**
```tsx
function Badge({ count }: { count: number }) {
return (
<div>
{count > 0 ? <span className="badge">{count}</span> : null}
</div>
)
}
// When count = 0, renders: <div></div>
// When count = 5, renders: <div><span class="badge">5</span></div>
```
================================================
FILE: .claude/skills/react-best-practices/rules/rendering-content-visibility.md
================================================
---
title: CSS content-visibility for Long Lists
impact: HIGH
impactDescription: faster initial render
tags: rendering, css, content-visibility, long-lists
---
## CSS content-visibility for Long Lists
Apply `content-visibility: auto` to defer off-screen rendering.
**CSS:**
```css
.message-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
```
**Example:**
```tsx
function MessageList({ messages }: { messages: Message[] }) {
return (
<div className="overflow-y-auto h-screen">
{messages.map(msg => (
<div key={msg.id} className="message-item">
<Avatar user={msg.author} />
<div>{msg.content}</div>
</div>
))}
</div>
)
}
```
For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
================================================
FILE: .claude/skills/react-best-practices/rules/rendering-hoist-jsx.md
================================================
---
title: Hoist Static JSX Elements
impact: LOW
impactDescription: avoids re-creation
tags: rendering, jsx, static, optimization
---
## Hoist Static JSX Elements
Extract static JSX outside components to avoid re-creation.
**Incorrect (recreates element every render):**
```tsx
function LoadingSkeleton() {
return <div className="animate-pulse h-20 bg-gray-200" />
}
function Container() {
return (
<div>
{loading && <LoadingSkeleton />}
</div>
)
}
```
**Correct (reuses same element):**
```tsx
const loadingSkeleton = (
<div className="animate-pulse h-20 bg-gray-200" />
)
function Container() {
return (
<div>
{loading && loadingSkeleton}
</div>
)
}
```
This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
================================================
FILE: .claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md
================================================
---
title: Prevent Hydration Mismatch Without Flickering
impact: MEDIUM
impactDescription: avoids visual flicker and hydration errors
tags: rendering, ssr, hydration, localStorage, flicker
---
## Prevent Hydration Mismatch Without Flickering
When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
**Incorrect (breaks SSR):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
// localStorage is not available on server - throws error
const theme = localStorage.getItem('theme') || 'light'
return (
<div className={theme}>
{children}
</div>
)
}
```
Server-side rendering will fail because `localStorage` is undefined.
**Incorrect (visual flickering):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState('light')
useEffect(() => {
// Runs after hydration - causes visible flash
const stored = localStorage.getItem('theme')
if (stored) {
setTheme(stored)
}
}, [])
return (
<div className={theme}>
{children}
</div>
)
}
```
Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
**Correct (no flicker, no hydration mismatch):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
return (
<>
<div id="theme-wrapper">
{children}
</div>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
var theme = localStorage.getItem('theme') || 'light';
var el = document.getElementById('theme-wrapper');
if (el) el.className = theme;
} catch (e) {}
})();
`,
}}
/>
</>
)
}
```
The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
================================================
FILE: .claude/skills/react-best-practices/rules/rendering-svg-precision.md
================================================
---
title: Optimize SVG Precision
impact: LOW
impactDescription: reduces file size
tags: rendering, svg, optimization, svgo
---
## Optimize SVG Precision
Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
**Incorrect (excessive precision):**
```svg
<path d="M 10.293847 20.847362 L 30.938472 40.192837" />
```
**Correct (1 decimal place):**
```svg
<path d="M 10.3 20.8 L 30.9 40.2" />
```
**Automate with SVGO:**
```bash
npx svgo --precision=1 --multipass icon.svg
```
================================================
FILE: .claude/skills/react-best-practices/rules/rerender-defer-reads.md
================================================
---
title: Defer State Reads to Usage Point
impact: MEDIUM
impactDescription: avoids unnecessary subscriptions
tags: rerender, searchParams, localStorage, optimization
---
## Defer State Reads to Usage Point
Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
**Incorrect (subscribes to all searchParams changes):**
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const searchParams = useSearchParams()
const handleShare = () => {
const ref = searchParams.get('ref')
shareChat(chatId, { ref })
}
return <button onClick={handleShare}>Share</button>
}
```
**Correct (reads on demand, no subscription):**
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const handleShare = () => {
const params = new URLSearchParams(window.location.search)
const ref = params.get('ref')
shareChat(chatId, { ref })
}
return <button onClick={handleShare}>Share</button>
}
```
================================================
FILE: .claude/skills/react-best-practices/rules/rerender-dependencies.md
================================================
---
title: Narrow Effect Dependencies
impact: LOW
impactDescription: minimizes effect re-runs
tags: rerender, useEffect, dependencies, optimization
---
## Narrow Effect Dependencies
Specify primitive dependencies instead of objects to minimize effect re-runs.
**Incorrect (re-runs on any user field change):**
```tsx
useEffect(() => {
console.log(user.id)
}, [user])
```
**Correct (re-runs only when id changes):**
```tsx
useEffect(() => {
console.log(user.id)
}, [user.id])
```
**For derived state, compute outside effect:**
```tsx
// Incorrect: runs on width=767, 766, 765...
useEffect(() => {
if (width < 768) {
enableMobileMode()
}
}, [width])
// Correct: runs only on boolean transition
const isMobile = width < 768
useEffect(() => {
if (isMobile) {
enableMobileMode()
}
}, [isMobile])
```
================================================
FILE: .claude/skills/react-best-practices/rules/rerender-derived-state.md
================================================
---
title: Subscribe to Derived State
impact: MEDIUM
impactDescription: reduces re-render frequency
tags: rerender, derived-state, media-query, optimization
---
## Subscribe to Derived State
Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
**Incorrect (re-renders on every pixel change):**
```tsx
function Sidebar() {
const width = useWindowWidth() // updates continuously
const isMobile = width < 768
return <nav className={isMobile ? 'mobile' : 'desktop'}>
}
```
**Correct (re-renders only when boolean changes):**
```tsx
function Sidebar() {
const isMobile = useMediaQuery('(max-width: 767px)')
return <nav className={isMobile ? 'mobile' : 'desktop'}>
}
```
================================================
FILE: .claude/skills/react-best-practices/rules/rerender-functional-setstate.md
================================================
---
title: Use Functional setState Updates
impact: MEDIUM
impactDescription: prevents stale closures and unnecessary callback recreations
tags: react, hooks, useState, useCallback, callbacks, closures
---
## Use Functional setState Updates
When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
**Incorrect (requires state as dependency):**
```tsx
function TodoList() {
const [items, setItems] = useState(initialItems)
// Callback must depend on items, recreated on every items change
const addItems = useCallback((newItems: Item[]) => {
setItems([...items, ...newItems])
}, [items]) // ❌ items dependency causes recreations
// Risk of stale closure if dependency is forgotten
const removeItem = useCallback((id: string) => {
setItems(items.filter(item => item.id !== id))
}, []) // ❌ Missing items dependency - will use stale items!
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
}
```
The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
**Correct (stable callbacks, no stale closures):**
```tsx
function TodoList() {
const [items, setItems] = useState(initialItems)
// Stable callback, never recreated
const addItems = useCallback((newItems: Item[]) => {
setItems(curr => [...curr, ...newItems])
}, []) // ✅ No dependencies needed
// Always uses latest state, no stale closure risk
const removeItem = useCallback((id: string) => {
setItems(curr => curr.filter(item => item.id !== id))
}, []) // ✅ Safe and stable
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
}
```
**Benefits:**
1. **Stable callback references** - Callbacks don't need to be recreated when state changes
2. **No stale closures** - Always operates on the latest state value
3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
4. **Prevents bugs** - Eliminates the most common source of React closure bugs
**When to use functional updates:**
- Any setState that depends on the current state value
- Inside useCallback/useMemo when state is needed
- Event handlers that reference state
- Async operations that update state
**When direct updates are fine:**
- Setting state to a static value: `setCount(0)`
- Setting state from props/arguments only: `setName(newName)`
- State doesn't depend on previous value
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
================================================
FILE: .claude/skills/react-best-practices/rules/rerender-lazy-state-init.md
================================================
---
title: Use Lazy State Initialization
impact: MEDIUM
impactDescription: wasted computation on every render
tags: react, hooks, useState, performance, initialization
---
## Use Lazy State Initialization
Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
**Incorrect (runs on every render):**
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs on EVERY render, even after initialization
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
const [query, setQuery] = useState('')
// When query changes, buildSearchIndex runs again unnecessarily
return <SearchResults index={searchIndex} query={query} />
}
function UserProfile() {
// JSON.parse runs on every render
const [settings, setSettings] = useState(
JSON.parse(localStorage.getItem('settings') || '{}')
)
return <SettingsForm settings={settings} onChange={setSettings} />
}
```
**Correct (runs only once):**
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs ONLY on initial render
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
const [query, setQuery] = useState('')
return <SearchResults index={searchIndex} query={query} />
}
function UserProfile() {
// JSON.parse runs only on initial render
const [settings, setSettings] = useState(() => {
const stored = localStorage.getItem('settings')
return stored ? JSON.parse(stored) : {}
})
return <SettingsForm settings={settings} onChange={setSettings} />
}
```
Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
================================================
FILE: .claude/skills/react-best-practices/rules/rerender-memo.md
================================================
---
title: Extract to Memoized Components
impact: MEDIUM
impactDescription: enables early returns
tags: rerender, memo, useMemo, optimization
---
## Extract to Memoized Components
Extract expensive work into memoized components to enable early returns before computation.
**Incorrect (computes avatar even when loading):**
```tsx
function Profile({ user, loading }: Props) {
const avatar = useMemo(() => {
const id = computeAvatarId(user)
return <Avatar id={id} />
}, [user])
if (loading) return <Skeleton />
return <div>{avatar}</div>
}
```
**Correct (skips computation when loading):**
```tsx
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const id = useMemo(() => computeAvatarId(user), [user])
return <Avatar id={id} />
})
function Profile({ user, loading }: Props) {
if (loading) return <Skeleton />
return (
<div>
<UserAvatar user={user} />
</div>
)
}
```
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.
================================================
FILE: .claude/skills/react-best-practices/rules/rerender-transitions.md
================================================
---
title: Use Transitions for Non-Urgent Updates
impact: MEDIUM
impactDescription: maintains UI responsiveness
tags: rerender, transitions, startTransition, performance
---
## Use Transitions for Non-Urgent Updates
Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
**Incorrect (blocks UI on every scroll):**
```tsx
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
useEffect(() => {
const handler = () => setScrollY(window.scrollY)
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
}
```
**Correct (non-blocking updates):**
```tsx
import { startTransition } from 'react'
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
useEffect(() => {
const handler = () => {
startTransition(() => setScrollY(window.scrollY))
}
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
}
```
================================================
FILE: .claude/skills/react-best-practices/rules/server-after-nonblocking.md
================================================
---
title: Use after() for Non-Blocking Operations
impact: MEDIUM
impactDescription: faster response times
tags: server, async, logging, analytics, side-effects
---
## Use after() for Non-Blocking Operations
Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.
**Incorrect (blocks response):**
```tsx
import { logUserAction } from '@/app/utils'
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
// Logging blocks the response
const userAgent = request.headers.get('user-agent') || 'unknown'
await logUserAction({ userAgent })
return new Response(JSON.stringify({ status: 'success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}
```
**Correct (non-blocking):**
```tsx
import { after } from 'next/server'
import { headers, cookies } from 'next/headers'
import { logUserAction } from '@/app/utils'
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
// Log after response is sent
after(async () => {
const userAgent = (await headers()).get('user-agent') || 'unknown'
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
logUserAction({ sessionCookie, userAgent })
})
return new Response(JSON.stringify({ status: 'success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}
```
The response is sent immediately while logging happens in the background.
**Common use cases:**
- Analytics tracking
- Audit logging
- Sending notifications
- Cache invalidation
- Cleanup tasks
**Important notes:**
- `after()` runs even if the response fails or redirects
- Works in Server Actions, Route Handlers, and Server Components
Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
================================================
FILE: .claude/skills/react-best-practices/rules/server-cache-lru.md
================================================
---
title: Cross-Request LRU Caching
impact: HIGH
impactDescription: caches across requests
tags: server, cache, lru, cross-request
---
## Cross-Request LRU Caching
`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
**Implementation:**
```typescript
import { LRUCache } from 'lru-cache'
const cache = new LRUCache<string, any>({
max: 1000,
ttl: 5 * 60 * 1000 // 5 minutes
})
export async function getUser(id: string) {
const cached = cache.get(id)
if (cached) return cached
const user = await db.user.findUnique({ where: { id } })
cache.set(id, user)
return user
}
// Request 1: DB query, result cached
// Request 2: cache hit, no DB query
```
Use when sequential user actions hit multiple endpoints needing the same data within seconds.
**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
================================================
FILE: .claude/skills/react-best-practices/rules/server-cache-react.md
================================================
---
title: Per-Request Deduplication with React.cache()
impact: MEDIUM
impactDescription: deduplicates within request
tags: server, cache, react-cache, deduplication
---
## Per-Request Deduplication with React.cache()
Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
**Usage:**
```typescript
import { cache } from 'react'
export const getCurrentUser = cache(async () => {
const session = await auth()
if (!session?.user?.id) return null
return await db.user.findUnique({
where: { id: session.user.id }
})
})
```
Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
================================================
FILE: .claude/skills/react-best-practices/rules/server-parallel-fetching.md
================================================
---
title: Parallel Data Fetching with Component Composition
impact: CRITICAL
impactDescription: eliminates server-side waterfalls
tags: server, rsc, parallel-fetching, composition
---
## Parallel Data Fetching with Component Composition
React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
**Incorrect (Sidebar waits for Page's fetch to complete):**
```tsx
export default async function Page() {
const header = await fetchHeader()
return (
<div>
<div>{header}</div>
<Sidebar />
</div>
)
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
}
```
**Correct (both fetch simultaneously):**
```tsx
async function Header() {
const data = await fetchHeader()
return <div>{data}</div>
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
}
export default function Page() {
return (
<div>
<Header />
<Sidebar />
</div>
)
}
```
**Alternative with children prop:**
```tsx
async function Layout({ children }: { children: ReactNode }) {
const header = await fetchHeader()
return (
<div>
<div>{header}</div>
{children}
</div>
)
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
}
export default function Page() {
return (
<Layout>
<Sidebar />
</Layout>
)
}
```
================================================
FILE: .claude/skills/react-best-practices/rules/server-serialization.md
================================================
---
title: Minimize Serialization at RSC Boundaries
impact: HIGH
impactDescription: reduces data transfer size
tags: server, rsc, serialization, props
---
## Minimize Serialization at RSC Boundaries
The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.
**Incorrect (serializes all 50 fields):**
```tsx
async function Page() {
const user = await fetchUser() // 50 fields
return <Profile user={user} />
}
'use client'
function Profile({ user }: { user: User }) {
return <div>{user.name}</div> // uses 1 field
}
```
**Correct (serializes only 1 field):**
```tsx
async function Page() {
const user = await fetchUser()
return <Profile name={user.name} />
}
'use client'
function Profile({ name }: { name: string }) {
return <div>{name}</div>
}
```
================================================
FILE: .dockerignore
================================================
node_modules
.next
dist
.turbo
.git
*.log
.env*
!.env.template
================================================
FILE: .github/FUNDING.yml
================================================
github: ridafkih
================================================
FILE: .github/workflows/checks.yml
================================================
name: Check Code Standards
on:
pull_request:
branches: [main]
jobs:
types:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run types
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run lint
unused:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run unused
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run test
================================================
FILE: .github/workflows/docker-publish.yml
================================================
name: Publish Docker Images
on:
push:
tags:
- "v*.*.*"
concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: false
env:
REGISTRY: ghcr.io
jobs:
build-core:
strategy:
matrix:
arch: [amd64, arm64]
package:
- name: api
dockerfile: services/api/Dockerfile
- name: cron
dockerfile: services/cron/Dockerfile
- name: worker
dockerfile: services/worker/Dockerfile
- name: mcp
dockerfile: services/mcp/Dockerfile
- name: web
dockerfile: applications/web/Dockerfile
include:
- arch: amd64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/keeper-${{ matrix.package.name }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-') }}
type=raw,value=latest,enable=${{ !contains(github.ref, '-') }}
flavor: suffix=-${{ matrix.arch }}
- uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.package.dockerfile }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.package.name }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.package.name }}-${{ matrix.arch }}
build-convenience:
strategy:
matrix:
package: [standalone, services]
arch: [amd64, arm64]
include:
- arch: amd64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/keeper-${{ matrix.package }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-') }}
type=raw,value=latest,enable=${{ !contains(github.ref, '-') }}
flavor: suffix=-${{ matrix.arch }}
- uses: docker/build-push-action@v6
with:
context: .
file: docker/${{ matrix.package }}/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.package }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.package }}-${{ matrix.arch }}
merge-core-manifests:
runs-on: ubuntu-latest
needs: build-core
permissions:
contents: read
packages: write
strategy:
matrix:
package:
- api
- cron
- worker
- mcp
- web
steps:
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/keeper-${{ matrix.package }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-') }}
type=raw,value=latest,enable=${{ !contains(github.ref, '-') }}
- name: Create multi-arch manifest
env:
TAGS: ${{ steps.meta.outputs.tags }}
run: |
for tag in $TAGS; do
docker buildx imagetools create -t "$tag" \
"${tag}-amd64" \
"${tag}-arm64"
done
merge-convenience-manifests:
runs-on: ubuntu-latest
needs: build-convenience
permissions:
contents: read
packages: write
strategy:
matrix:
package: [standalone, services]
steps:
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/keeper-${{ matrix.package }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-') }}
type=raw,value=latest,enable=${{ !contains(github.ref, '-') }}
- name: Create multi-arch manifest
env:
TAGS: ${{ steps.meta.outputs.tags }}
run: |
for tag in $TAGS; do
docker buildx imagetools create -t "$tag" \
"${tag}-amd64" \
"${tag}-arm64"
done
deploy-ec2:
runs-on: ubuntu-latest
needs: merge-core-manifests
if: ${{ !contains(github.ref, '-') }}
concurrency:
group: production-deploy
cancel-in-progress: false
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_SSH_KEY }}
source: "deploy/compose.yaml,deploy/Caddyfile,docker/caddy/Dockerfile"
target: /home/ubuntu
- uses: appleboy/ssh-action@v1.2.4
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_SSH_KEY }}
script: |
set -euo pipefail
exec 9>/tmp/keeper-deploy.lock
flock 9
cd /home/ubuntu
cd deploy
docker compose pull
docker compose up -d --build --remove-orphans --wait
docker image prune -af --filter "until=24h"
================================================
FILE: .gitignore
================================================
node_modules
.env
.idea
.DS_Store
.turbo/
dist/
*.tsbuildinfo
screenshots/
applications/mobile/
.pki/
================================================
FILE: .oxlintrc.json
================================================
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["import", "typescript", "unicorn", "promise"],
"env": {
"browser": true,
"node": true,
"es2024": true
},
"globals": {
"Bun": "readonly"
},
"categories": {
"correctness": "error",
"suspicious": "error",
"pedantic": "error",
"perf": "error",
"style": "error",
"restriction": "error"
},
"rules": {
"eqeqeq": ["error", "always"],
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"import/no-cycle": "error",
"import/no-self-import": "error",
"import/no-named-export": "off",
"import/prefer-default-export": "off",
"import/max-dependencies": "off",
"import/no-anonymous-default-export": "off",
"import/consistent-type-specifier-style": "off",
"import/no-duplicates": "off",
"import/no-nodejs-modules": "off",
"import/no-relative-parent-imports": "off",
"unicorn/no-null": "off",
"unicorn/prefer-global-this": "off",
"unicorn/prefer-top-level-await": "off",
"unicorn/no-process-exit": "off",
"react/jsx-max-depth": "off",
"react/react-in-jsx-scope": "off",
"react/jsx-filename-extension": "off",
"react/no-unescaped-entities": "off",
"react/jsx-props-no-spreading": "off",
"promise/avoid-new": "off",
"promise/prefer-await-to-then": "off",
"promise/prefer-await-to-callbacks": "off",
"no-duplicate-imports": "off",
"no-default-export": "off",
"no-await-in-loop": "off",
"no-plusplus": "off",
"no-continue": "off",
"no-magic-numbers": "off",
"no-warning-comments": "off",
"no-eq-null": "off",
"max-lines-per-function": "off",
"max-statements": "off",
"max-lines": "off",
"max-params": "off",
"max-classes-per-file": "off",
"sort-imports": "off",
"sort-keys": "off",
"id-length": ["error", { "exceptions": ["x", "y"] }]
},
"ignorePatterns": [
"**/node_modules/**",
"**/dist/**",
"**/.next/**",
"**/.turbo/**",
"**/coverage/**"
]
}
================================================
FILE: Caddyfile
================================================
keeper.localhost {
tls internal
reverse_proxy host.docker.internal:5173
}
:80 {
reverse_proxy host.docker.internal:5173
}
================================================
FILE: LICENSE
================================================
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.
================================================
FILE: NOTICE
================================================
Copyright (C) 2025 Rida F'kih
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
================================================
FILE: README.md
================================================

# About
Keeper is a simple & open-source calendar syncing tool. It allows you to pull events from remotely hosted iCal or ICS links, and push them to one or many calendars so the time slots can align across them all.
# Features
- Aggregating calendar events from remote sources
- Event content agnostic syncing engine
- Push aggregate events to one or more calendars
- MCP (Model Context Protocol) server for AI agent calendar access
- Open source under AGPL-3.0
- Easy to self-host
- Easy-to-purge remote events
# Bug Reports & Feature Requests
If you encounter a bug or have an idea for a feature, you may [open an issue on GitHub](https://github.com/ridafkih/keeper.sh/issues) and it will be triaged and addressed as soon as possible.
# Contributing
High-value and high-quality contributions are appreciated. Before working on large features you intend to see merged, please open an issue first to discuss beforehand.
## Local Development
The dev environment runs behind HTTPS at `https://keeper.localhost` using a [Caddy](https://caddyserver.com/) reverse proxy with automatic TLS. The `.localhost` TLD resolves to `127.0.0.1` automatically per [RFC 6761](https://datatracker.ietf.org/doc/html/rfc6761) — no `/etc/hosts` entry is needed.
### Prerequisites
- [Bun](https://bun.sh/) (v1.3.11+)
- [Docker](https://docs.docker.com/get-started/) & Docker Compose
### Getting Started
```bash
bun install
```
#### Generate and Trust a Root CA
The dev environment runs behind HTTPS via Caddy. You need to generate a local root certificate authority and trust it so your browser accepts the certificate.
```bash
mkdir -p .pki
openssl req -x509 -new -nodes \
-newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
-keyout .pki/root.key -out .pki/root.crt \
-days 3650 -subj "/CN=Keeper.sh CA"
```
Then trust it on your platform:
**macOS**
```bash
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain .pki/root.crt
```
**Linux**
```bash
sudo cp .pki/root.crt /usr/local/share/ca-certificates/keeper-dev-root.crt
sudo update-ca-certificates
```
#### Start the Dev Environment
```bash
bun dev
```
This starts PostgreSQL, Redis, and a Caddy reverse proxy via Docker Compose, along with the API, web, MCP, and cron services locally. Once running, open `https://keeper.localhost`.
### Architecture
| Service | Local Port | Accessed Via |
| -------- | ---------- | ------------------------------------ |
| Caddy | 443 | `https://keeper.localhost` |
| Web | 5173 | Proxied by Caddy |
| API | 3000 | Proxied by Web at `/api` |
| MCP | 3001 | Proxied by Web at `/mcp` |
| Postgres | 5432 | `postgresql://postgres:postgres@localhost:5432/postgres` |
| Redis | 6379 | `redis://localhost:6379` |
# Qs
## Why does this exist?
Because I needed it. Ever since starting [Sedna](https://sedna.sh/)—the AI governance platform—I've had to work across three calendars. One for my business, one for work, and one for personal.
Meetings have landed on top of one-another a frustratingly high number of times.
## Why not use _this other service_?
I've probably tried it. It was probably too finicky, ended up making me waste hours of my time having to delete stale events it didn't seem to want to track anymore, or just didn't sync reliably.
## How does the syncing engine work?
- If we have a local event but no corresponding "source → destination" mapping for an event, we push the event to the destination calendar.
- If we have a mapping for an event, but the source ID is not present on the source any longer, we delete the event from the destination.
- Any events with markers of having been created by Keeper, but with no corresponding local tracking, we remove it. This is only done for backwards compatibility.
Events are flagged as having been created by Keeper either using a `@keeper.sh` suffix on the remote UID, or in the case of a platform like Outlook that doesn't support custom UIDs, we just put it in a `"keeper.sh"` category.
# Cloud Hosted
I've made Keeper easy to self-host, but whether you simply want to support the project or don't want to deal with the hassle or overhead of configuring and running your own infrastructure cloud hosting is always an option.
Head to [keeper.sh](https://keeper.sh) to get started with the cloud-hosted version. Use code `README` for 25% off.
| | Free | Pro (Cloud-Hosted) | Pro (Self-Hosted) |
| --------------------- | ---------- | ------------------ | ----------------- |
| **Monthly Price** | $0 USD | $5 USD | $0 |
| **Annual Price** | $0 USD | $42 USD (-30%) | $0 |
| **Refresh Interval** | 30 minutes | 1 minute | 1 minute |
| **Source Limit** | 2 | ∞ | ∞ |
| **Destination Limit** | 1 | ∞ | ∞ |
# Self Hosted
By hosting Keeper yourself, you get all premium features for free, can guarantee data governance and autonomy, and it's fun. If you'll be self-hosting, please consider supporting me and development of the project by sponsoring me on GitHub.
There are seven images currently available, two of them are designed for convenience, while the five are designed to serve the granular underlying services.
> [!NOTE]
>
> **Migrating from a previous version?** If you are upgrading from the older Next.js-based release, see the [migration guide](https://github.com/ridafkih/keeper.sh/issues/140) for environment variable changes. The new web server will also print a migration notice at startup if it detects old environment variables.
## Environment Variables
| Name | Service(s) | Description |
| ------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| DATABASE_URL | `api`, `cron`, `worker`, `mcp` | PostgreSQL connection URL.<br><br>e.g. `postgres://user:pass@postgres:5432/keeper` |
| REDIS_URL | `api`, `cron`, `worker` | Redis connection URL. Must be the same Redis instance across all services.<br><br>e.g. `redis://redis:6379` |
| WORKER_JOB_QUEUE_ENABLED | `cron` | Required. Set to `true` to enqueue sync jobs to the worker queue, or `false` to disable. If unset, the cron service will exit with a migration notice. |
| BETTER_AUTH_URL | `api`, `mcp` | The base URL used for auth redirects.<br><br>e.g. `http://localhost:3000` |
| BETTER_AUTH_SECRET | `api`, `mcp` | Secret key for session signing.<br><br>e.g. `openssl rand -base64 32` |
| API_PORT | `api` | Port the Bun API listens on. Defaults to `3001` in container images. |
| ENV | `web` | Optional. Runtime environment. One of `development`, `production`, or `test`. Defaults to `production`. |
| PORT | `web` | Port the web server listens on. Defaults to `3000` in container images. |
| VITE_API_URL | `web` | The URL the web server uses to proxy requests to the Bun API.<br><br>e.g. `http://api:3001` |
| COMMERCIAL_MODE | `api`, `cron` | Enable Polar billing flow. Set to `true` if using Polar for subscriptions. |
| POLAR_ACCESS_TOKEN | `api`, `cron` | Optional. Polar API token for subscription management. |
| POLAR_MODE | `api`, `cron` | Optional. Polar environment, `sandbox` or `production`. |
| POLAR_WEBHOOK_SECRET | `api` | Optional. Secret to verify Polar webhooks. |
| ENCRYPTION_KEY | `api`, `cron`, `worker` | Key for encrypting CalDAV credentials at rest.<br><br>e.g. `openssl rand -base64 32` |
| RESEND_API_KEY | `api` | Optional. API key for sending emails via Resend. |
| PASSKEY_RP_ID | `api` | Optional. Relying party ID for passkey authentication. |
| PASSKEY_RP_NAME | `api` | Optional. Relying party display name for passkeys. |
| PASSKEY_ORIGIN | `api` | Optional. Origin allowed for passkey flows (e.g., `https://keeper.example.com`). |
| GOOGLE_CLIENT_ID | `api`, `cron`, `worker` | Optional. Required for Google Calendar integration. |
| GOOGLE_CLIENT_SECRET | `api`, `cron`, `worker` | Optional. Required for Google Calendar integration. |
| MICROSOFT_CLIENT_ID | `api`, `cron`, `worker` | Optional. Required for Microsoft Outlook integration. |
| MICROSOFT_CLIENT_SECRET | `api`, `cron`, `worker` | Optional. Required for Microsoft Outlook integration. |
| POSTGRES_PASSWORD | `standalone` | Optional. Custom password for the internal PostgreSQL database in `keeper-standalone`. If unset, defaults to `keeper`. The database is not exposed outside the container, so this is low risk, but can be set for defense in depth. |
| BLOCK_PRIVATE_RESOLUTION | `api`, `cron` | Optional. Set to `true` to block outbound fetches (ICS subscriptions, CalDAV servers) from resolving to private/reserved network addresses. Prevents SSRF. Defaults to `false` for backward compatibility with self-hosted setups that use local CalDAV/ICS servers. |
| PRIVATE_RESOLUTION_WHITELIST | `api`, `cron` | Optional. When `BLOCK_PRIVATE_RESOLUTION` is `true`, this comma-separated list of hostnames or IPs is exempt from the restriction.<br><br>e.g. `192.168.1.50,radicale.local,10.0.2.12` |
| TRUSTED_ORIGINS | `api` | Optional. Comma-separated list of additional trusted origins for CSRF protection.<br><br>e.g. `http://192.168.1.100,http://keeper.local,https://keeper.example.com` |
| MCP_PUBLIC_URL | `api`, `mcp` | Optional. Public URL of the MCP resource. Enables OAuth on the API and identifies the MCP server to clients.<br><br>e.g. `https://keeper.example.com/mcp` |
| VITE_MCP_URL | `web` | Optional. Internal URL the web server uses to proxy `/mcp` requests to the MCP service.<br><br>e.g. `http://mcp:3002` |
| MCP_PORT | `mcp` | Optional. Port the MCP server listens on.<br><br>e.g. `3002` |
| OTEL_EXPORTER_OTLP_ENDPOINT | `api`, `cron`, `worker`, `mcp`, `web` | Optional. When set, enables forwarding structured logs to an OpenTelemetry collector via [`pino-opentelemetry-transport`](https://github.com/Vunovati/pino-opentelemetry-transport). The transport runs in a dedicated worker thread and does not affect application performance.<br><br>e.g. `https://otel-collector.example.com:4318` |
| OTEL_EXPORTER_OTLP_PROTOCOL | `api`, `cron`, `worker`, `mcp`, `web` | Optional. Protocol used by the OTLP exporter. Defaults to `http/protobuf` per the Open
gitextract_47nb8t9x/ ├── .claude/ │ ├── settings.json │ └── skills/ │ └── react-best-practices/ │ ├── AGENTS.md │ ├── README.md │ ├── SKILL.md │ ├── metadata.json │ └── rules/ │ ├── _sections.md │ ├── _template.md │ ├── advanced-event-handler-refs.md │ ├── advanced-use-latest.md │ ├── async-api-routes.md │ ├── async-defer-await.md │ ├── async-dependencies.md │ ├── async-parallel.md │ ├── async-suspense-boundaries.md │ ├── bundle-barrel-imports.md │ ├── bundle-conditional.md │ ├── bundle-defer-third-party.md │ ├── bundle-dynamic-imports.md │ ├── bundle-preload.md │ ├── client-event-listeners.md │ ├── client-swr-dedup.md │ ├── js-batch-dom-css.md │ ├── js-cache-function-results.md │ ├── js-cache-property-access.md │ ├── js-cache-storage.md │ ├── js-combine-iterations.md │ ├── js-early-exit.md │ ├── js-hoist-regexp.md │ ├── js-index-maps.md │ ├── js-length-check-first.md │ ├── js-min-max-loop.md │ ├── js-set-map-lookups.md │ ├── js-tosorted-immutable.md │ ├── rendering-activity.md │ ├── rendering-animate-svg-wrapper.md │ ├── rendering-conditional-render.md │ ├── rendering-content-visibility.md │ ├── rendering-hoist-jsx.md │ ├── rendering-hydration-no-flicker.md │ ├── rendering-svg-precision.md │ ├── rerender-defer-reads.md │ ├── rerender-dependencies.md │ ├── rerender-derived-state.md │ ├── rerender-functional-setstate.md │ ├── rerender-lazy-state-init.md │ ├── rerender-memo.md │ ├── rerender-transitions.md │ ├── server-after-nonblocking.md │ ├── server-cache-lru.md │ ├── server-cache-react.md │ ├── server-parallel-fetching.md │ └── server-serialization.md ├── .dockerignore ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── checks.yml │ └── docker-publish.yml ├── .gitignore ├── .oxlintrc.json ├── Caddyfile ├── LICENSE ├── NOTICE ├── README.md ├── applications/ │ └── web/ │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── entrypoint.sh │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── plugins/ │ │ ├── blog.ts │ │ └── sitemap.ts │ ├── public/ │ │ ├── llms-full.txt │ │ ├── llms.txt │ │ ├── robots.txt │ │ └── site.webmanifest │ ├── scripts/ │ │ ├── build.ts │ │ └── start.ts │ ├── src/ │ │ ├── components/ │ │ │ ├── analytics-scripts.tsx │ │ │ ├── cookie-consent.tsx │ │ │ └── ui/ │ │ │ ├── composites/ │ │ │ │ └── navigation-menu/ │ │ │ │ ├── navigation-menu-editable.tsx │ │ │ │ ├── navigation-menu-items.tsx │ │ │ │ ├── navigation-menu-popover.tsx │ │ │ │ ├── navigation-menu.contexts.ts │ │ │ │ └── navigation-menu.styles.ts │ │ │ ├── primitives/ │ │ │ │ ├── animated-reveal.tsx │ │ │ │ ├── back-button.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── collapsible.tsx │ │ │ │ ├── dashboard-heading.tsx │ │ │ │ ├── delete-confirmation.tsx │ │ │ │ ├── divider.tsx │ │ │ │ ├── error-state.tsx │ │ │ │ ├── fade-in.tsx │ │ │ │ ├── github-star-button.tsx │ │ │ │ ├── heading.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── list.tsx │ │ │ │ ├── markdown-component-map.ts │ │ │ │ ├── markdown-components.tsx │ │ │ │ ├── modal.tsx │ │ │ │ ├── pagination.tsx │ │ │ │ ├── provider-icon-stack.tsx │ │ │ │ ├── provider-icon.tsx │ │ │ │ ├── shimmer-text.tsx │ │ │ │ ├── staggered-backdrop-blur.tsx │ │ │ │ ├── template-text.tsx │ │ │ │ ├── text-link.tsx │ │ │ │ ├── text.tsx │ │ │ │ ├── tooltip.tsx │ │ │ │ └── upgrade-hint.tsx │ │ │ └── shells/ │ │ │ ├── layout.tsx │ │ │ ├── route-shell.tsx │ │ │ └── session-slot.tsx │ │ ├── config/ │ │ │ ├── commercial.ts │ │ │ ├── gdpr.ts │ │ │ └── plans.ts │ │ ├── content/ │ │ │ └── blog/ │ │ │ └── introducing-keeper-blog.mdx │ │ ├── features/ │ │ │ ├── auth/ │ │ │ │ └── components/ │ │ │ │ ├── auth-form.tsx │ │ │ │ ├── auth-switch-prompt.tsx │ │ │ │ ├── caldav-connect-form.tsx │ │ │ │ ├── caldav-connect-page.tsx │ │ │ │ ├── ics-connect-form.tsx │ │ │ │ └── oauth-preamble.tsx │ │ │ ├── blog/ │ │ │ │ └── components/ │ │ │ │ └── blog-post-cta.tsx │ │ │ ├── dashboard/ │ │ │ │ └── components/ │ │ │ │ ├── event-graph.tsx │ │ │ │ ├── metadata-row.tsx │ │ │ │ ├── sync-status-helpers.ts │ │ │ │ ├── sync-status.tsx │ │ │ │ └── upgrade-card.tsx │ │ │ └── marketing/ │ │ │ ├── components/ │ │ │ │ ├── marketing-cta.tsx │ │ │ │ ├── marketing-faq.tsx │ │ │ │ ├── marketing-feature-bento.tsx │ │ │ │ ├── marketing-footer.tsx │ │ │ │ ├── marketing-header.tsx │ │ │ │ ├── marketing-how-it-works.tsx │ │ │ │ ├── marketing-illustration-calendar.tsx │ │ │ │ └── marketing-pricing-section.tsx │ │ │ └── contributors.json │ │ ├── generated/ │ │ │ └── tanstack/ │ │ │ └── route-tree.generated.ts │ │ ├── hooks/ │ │ │ ├── use-animated-swr.ts │ │ │ ├── use-api-tokens.ts │ │ │ ├── use-entitlements.ts │ │ │ ├── use-events.ts │ │ │ ├── use-has-password.ts │ │ │ ├── use-passkeys.ts │ │ │ ├── use-session.ts │ │ │ ├── use-start-of-today.ts │ │ │ └── use-subscription.ts │ │ ├── illustrations/ │ │ │ ├── how-it-works-configure.tsx │ │ │ ├── how-it-works-connect.tsx │ │ │ ├── how-it-works-sync.tsx │ │ │ ├── marketing-illustration-contributors.tsx │ │ │ ├── marketing-illustration-providers.tsx │ │ │ ├── marketing-illustration-setup.tsx │ │ │ └── marketing-illustration-sync.tsx │ │ ├── index.css │ │ ├── index.d.ts │ │ ├── lib/ │ │ │ ├── analytics.ts │ │ │ ├── auth-capabilities.ts │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── blog-posts.ts │ │ │ ├── fetcher.ts │ │ │ ├── mcp-auth-flow.ts │ │ │ ├── motion-features.ts │ │ │ ├── page-metadata.ts │ │ │ ├── pluralize.ts │ │ │ ├── providers.ts │ │ │ ├── route-access-guards.ts │ │ │ ├── router-context.ts │ │ │ ├── runtime-config.ts │ │ │ ├── seo.ts │ │ │ ├── serialized-mutate.ts │ │ │ ├── session-cookie.ts │ │ │ ├── swr.ts │ │ │ └── time.ts │ │ ├── main.tsx │ │ ├── providers/ │ │ │ ├── sync-provider-logic.ts │ │ │ └── sync-provider.tsx │ │ ├── routeTree.gen.ts │ │ ├── router.ts │ │ ├── routes/ │ │ │ ├── (auth)/ │ │ │ │ ├── forgot-password.tsx │ │ │ │ ├── login.tsx │ │ │ │ ├── register.tsx │ │ │ │ ├── reset-password.tsx │ │ │ │ ├── route.tsx │ │ │ │ ├── verify-authentication.tsx │ │ │ │ └── verify-email.tsx │ │ │ ├── (dashboard)/ │ │ │ │ ├── dashboard/ │ │ │ │ │ ├── accounts/ │ │ │ │ │ │ ├── $accountId.$calendarId.tsx │ │ │ │ │ │ ├── $accountId.index.tsx │ │ │ │ │ │ ├── $accountId.setup.tsx │ │ │ │ │ │ └── route.tsx │ │ │ │ │ ├── connect/ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── route.tsx │ │ │ │ │ ├── events/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── feedback.tsx │ │ │ │ │ ├── ical.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── integrations/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── report.tsx │ │ │ │ │ ├── settings/ │ │ │ │ │ │ ├── api-tokens.tsx │ │ │ │ │ │ ├── change-password.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── passkeys.tsx │ │ │ │ │ │ └── route.tsx │ │ │ │ │ └── upgrade/ │ │ │ │ │ └── index.tsx │ │ │ │ └── route.tsx │ │ │ ├── (marketing)/ │ │ │ │ ├── blog/ │ │ │ │ │ ├── $slug.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── route.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── privacy.tsx │ │ │ │ ├── route.tsx │ │ │ │ └── terms.tsx │ │ │ ├── (oauth)/ │ │ │ │ ├── auth/ │ │ │ │ │ ├── google.tsx │ │ │ │ │ ├── outlook.tsx │ │ │ │ │ └── route.tsx │ │ │ │ ├── dashboard/ │ │ │ │ │ ├── connect/ │ │ │ │ │ │ ├── apple.tsx │ │ │ │ │ │ ├── caldav.tsx │ │ │ │ │ │ ├── fastmail.tsx │ │ │ │ │ │ ├── google.tsx │ │ │ │ │ │ ├── ical-link.tsx │ │ │ │ │ │ ├── ics-file.tsx │ │ │ │ │ │ ├── microsoft.tsx │ │ │ │ │ │ ├── outlook.tsx │ │ │ │ │ │ └── route.tsx │ │ │ │ │ └── route.tsx │ │ │ │ ├── oauth/ │ │ │ │ │ └── consent.tsx │ │ │ │ └── route.tsx │ │ │ └── __root.tsx │ │ ├── server/ │ │ │ ├── cache/ │ │ │ │ └── stale-cache.ts │ │ │ ├── compression.ts │ │ │ ├── config.ts │ │ │ ├── github-stars.ts │ │ │ ├── http-handler.ts │ │ │ ├── index.ts │ │ │ ├── internal-routes.ts │ │ │ ├── logging.ts │ │ │ ├── migration-check.ts │ │ │ ├── paths.ts │ │ │ ├── proxy/ │ │ │ │ ├── http.ts │ │ │ │ └── websocket.ts │ │ │ ├── runtime.ts │ │ │ ├── types.ts │ │ │ └── vite-assets.ts │ │ ├── server.tsx │ │ ├── state/ │ │ │ ├── auth-form.ts │ │ │ ├── calendar-detail.ts │ │ │ ├── calendar-emphasized.ts │ │ │ ├── destination-ids.ts │ │ │ ├── event-graph-hover.ts │ │ │ ├── ical-feed-settings.ts │ │ │ ├── ical-sources.ts │ │ │ ├── popover-overlay.ts │ │ │ └── sync.ts │ │ ├── types/ │ │ │ └── api.ts │ │ ├── utils/ │ │ │ ├── calendars.ts │ │ │ ├── checkout.ts │ │ │ ├── cn.ts │ │ │ ├── collections.ts │ │ │ ├── errors.ts │ │ │ └── templates.ts │ │ └── vite-env.d.ts │ ├── tests/ │ │ ├── features/ │ │ │ ├── auth/ │ │ │ │ └── components/ │ │ │ │ └── auth-form.test.tsx │ │ │ └── dashboard/ │ │ │ └── components/ │ │ │ └── sync-status-helpers.test.ts │ │ ├── hooks/ │ │ │ └── use-entitlements.test.ts │ │ ├── lib/ │ │ │ ├── auth-capabilities.test.ts │ │ │ ├── auth.test.ts │ │ │ ├── mcp-auth-flow.test.ts │ │ │ ├── route-access-guards.test.ts │ │ │ ├── runtime-config.test.ts │ │ │ └── serialized-mutate.test.ts │ │ ├── providers/ │ │ │ └── sync-provider-logic.test.ts │ │ ├── server/ │ │ │ └── internal-routes.test.ts │ │ └── state/ │ │ └── sync.test.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── vitest.config.ts ├── bunfig.toml ├── compose.yaml ├── deploy/ │ ├── Caddyfile │ └── compose.yaml ├── docker/ │ ├── caddy/ │ │ └── Dockerfile │ ├── services/ │ │ ├── Dockerfile │ │ └── rootfs/ │ │ └── etc/ │ │ └── s6-overlay/ │ │ ├── s6-rc.d/ │ │ │ ├── api/ │ │ │ │ ├── dependencies.d/ │ │ │ │ │ └── init-db │ │ │ │ ├── run │ │ │ │ └── type │ │ │ ├── cron/ │ │ │ │ ├── dependencies.d/ │ │ │ │ │ └── init-db │ │ │ │ ├── run │ │ │ │ └── type │ │ │ ├── init-db/ │ │ │ │ ├── type │ │ │ │ └── up │ │ │ ├── user/ │ │ │ │ ├── contents.d/ │ │ │ │ │ ├── api │ │ │ │ │ ├── cron │ │ │ │ │ ├── init-db │ │ │ │ │ ├── web │ │ │ │ │ └── worker │ │ │ │ └── type │ │ │ ├── web/ │ │ │ │ ├── dependencies.d/ │ │ │ │ │ └── api │ │ │ │ ├── run │ │ │ │ └── type │ │ │ └── worker/ │ │ │ ├── dependencies.d/ │ │ │ │ └── init-db │ │ │ ├── run │ │ │ └── type │ │ └── scripts/ │ │ └── init-db │ └── standalone/ │ ├── Dockerfile │ └── rootfs/ │ └── etc/ │ ├── caddy/ │ │ └── Caddyfile │ └── s6-overlay/ │ ├── s6-rc.d/ │ │ ├── api/ │ │ │ ├── dependencies.d/ │ │ │ │ ├── init-db │ │ │ │ └── redis │ │ │ ├── run │ │ │ └── type │ │ ├── caddy/ │ │ │ ├── dependencies.d/ │ │ │ │ └── web │ │ │ ├── run │ │ │ └── type │ │ ├── cron/ │ │ │ ├── dependencies.d/ │ │ │ │ ├── init-db │ │ │ │ └── redis │ │ │ ├── run │ │ │ └── type │ │ ├── init-db/ │ │ │ ├── dependencies.d/ │ │ │ │ └── postgres │ │ │ ├── type │ │ │ └── up │ │ ├── postgres/ │ │ │ ├── dependencies.d/ │ │ │ │ └── base │ │ │ ├── run │ │ │ └── type │ │ ├── redis/ │ │ │ ├── dependencies.d/ │ │ │ │ └── base │ │ │ ├── run │ │ │ └── type │ │ ├── user/ │ │ │ ├── contents.d/ │ │ │ │ ├── api │ │ │ │ ├── caddy │ │ │ │ ├── cron │ │ │ │ ├── init-db │ │ │ │ ├── postgres │ │ │ │ ├── redis │ │ │ │ ├── web │ │ │ │ └── worker │ │ │ └── type │ │ ├── web/ │ │ │ ├── dependencies.d/ │ │ │ │ └── api │ │ │ ├── run │ │ │ └── type │ │ └── worker/ │ │ ├── dependencies.d/ │ │ │ ├── init-db │ │ │ └── redis │ │ ├── run │ │ └── type │ └── scripts/ │ └── init-db ├── knip.json ├── lefthook.yml ├── package.json ├── packages/ │ ├── auth/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── capabilities.ts │ │ │ ├── index.ts │ │ │ ├── mcp-config.ts │ │ │ ├── plugins/ │ │ │ │ └── username-only/ │ │ │ │ ├── endpoints/ │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ └── sign-up.ts │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils/ │ │ │ │ ├── config.ts │ │ │ │ └── schema.ts │ │ │ ├── polar-customer-delete.ts │ │ │ └── runtime-environment.ts │ │ ├── tests/ │ │ │ ├── capabilities.test.ts │ │ │ ├── mcp-config.test.ts │ │ │ └── polar-customer-delete.test.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── broadcast/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── state.ts │ │ │ └── types.ts │ │ └── tsconfig.json │ ├── calendar/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── core/ │ │ │ │ ├── events/ │ │ │ │ │ ├── all-day.ts │ │ │ │ │ ├── content-hash.ts │ │ │ │ │ ├── events.ts │ │ │ │ │ ├── identity.ts │ │ │ │ │ ├── mappings.ts │ │ │ │ │ └── recurrence.ts │ │ │ │ ├── oauth/ │ │ │ │ │ ├── accounts.ts │ │ │ │ │ ├── config.ts │ │ │ │ │ ├── coordinated-refresher.ts │ │ │ │ │ ├── create-source-provider.ts │ │ │ │ │ ├── ensure-valid-token.ts │ │ │ │ │ ├── error-classification.ts │ │ │ │ │ ├── google.ts │ │ │ │ │ ├── microsoft.ts │ │ │ │ │ ├── providers.ts │ │ │ │ │ ├── refresh-coordinator.ts │ │ │ │ │ ├── source-provider.ts │ │ │ │ │ ├── state.ts │ │ │ │ │ ├── sync-token.ts │ │ │ │ │ ├── sync-window.ts │ │ │ │ │ └── token-provider.ts │ │ │ │ ├── source/ │ │ │ │ │ ├── event-diff.ts │ │ │ │ │ ├── sync-diagnostics.ts │ │ │ │ │ └── write-event-states.ts │ │ │ │ ├── sync/ │ │ │ │ │ ├── aggregate-runtime.ts │ │ │ │ │ ├── aggregate-tracker.ts │ │ │ │ │ ├── operations.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── sync-engine/ │ │ │ │ │ ├── flush.ts │ │ │ │ │ ├── generation.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ingest.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils/ │ │ │ │ ├── concurrency.ts │ │ │ │ ├── error.ts │ │ │ │ ├── rate-limiter.ts │ │ │ │ └── redis-rate-limiter.ts │ │ │ ├── ics/ │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils/ │ │ │ │ ├── create-snapshot.ts │ │ │ │ ├── diff-events.ts │ │ │ │ ├── fetch-adapter.ts │ │ │ │ ├── normalize-timezone.ts │ │ │ │ ├── parse-ics-calendar.ts │ │ │ │ ├── parse-ics-events.ts │ │ │ │ ├── pull-remote-calendar.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ ├── providers/ │ │ │ │ ├── caldav/ │ │ │ │ │ ├── destination/ │ │ │ │ │ │ └── provider.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── shared/ │ │ │ │ │ │ ├── client.ts │ │ │ │ │ │ ├── digest-fetch.ts │ │ │ │ │ │ ├── ics.ts │ │ │ │ │ │ └── sync-window.ts │ │ │ │ │ ├── source/ │ │ │ │ │ │ ├── auth-error-classification.ts │ │ │ │ │ │ ├── fetch-adapter.ts │ │ │ │ │ │ ├── provider.ts │ │ │ │ │ │ ├── sync-window.ts │ │ │ │ │ │ └── sync.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── fastmail/ │ │ │ │ │ ├── destination/ │ │ │ │ │ │ └── provider.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── source/ │ │ │ │ │ └── provider.ts │ │ │ │ ├── google/ │ │ │ │ │ ├── destination/ │ │ │ │ │ │ ├── provider.ts │ │ │ │ │ │ ├── serialize-event.ts │ │ │ │ │ │ └── sync.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── shared/ │ │ │ │ │ │ ├── api.ts │ │ │ │ │ │ ├── backoff.ts │ │ │ │ │ │ ├── batch.ts │ │ │ │ │ │ ├── date-time.ts │ │ │ │ │ │ └── errors.ts │ │ │ │ │ ├── source/ │ │ │ │ │ │ ├── fetch-adapter.ts │ │ │ │ │ │ ├── provider.ts │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ └── utils/ │ │ │ │ │ │ ├── fetch-events.ts │ │ │ │ │ │ └── list-calendars.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── icloud/ │ │ │ │ │ ├── destination/ │ │ │ │ │ │ └── provider.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── source/ │ │ │ │ │ └── provider.ts │ │ │ │ └── outlook/ │ │ │ │ ├── destination/ │ │ │ │ │ ├── provider.ts │ │ │ │ │ ├── serialize-event.ts │ │ │ │ │ └── sync.ts │ │ │ │ ├── index.ts │ │ │ │ ├── shared/ │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── date-time.ts │ │ │ │ │ └── errors.ts │ │ │ │ ├── source/ │ │ │ │ │ ├── fetch-adapter.ts │ │ │ │ │ ├── provider.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils/ │ │ │ │ │ ├── fetch-events.ts │ │ │ │ │ └── list-calendars.ts │ │ │ │ └── types.ts │ │ │ └── utils/ │ │ │ ├── registry/ │ │ │ │ ├── registry.ts │ │ │ │ └── server.ts │ │ │ └── safe-fetch.ts │ │ ├── tests/ │ │ │ ├── core/ │ │ │ │ ├── events/ │ │ │ │ │ ├── content-hash.test.ts │ │ │ │ │ ├── events.test.ts │ │ │ │ │ ├── identity.test.ts │ │ │ │ │ └── recurrence.test.ts │ │ │ │ ├── oauth/ │ │ │ │ │ ├── ensure-valid-token.test.ts │ │ │ │ │ ├── error-classification.test.ts │ │ │ │ │ ├── google.test.ts │ │ │ │ │ ├── refresh-coordinator.test.ts │ │ │ │ │ ├── sync-token.test.ts │ │ │ │ │ └── sync-window.test.ts │ │ │ │ ├── source/ │ │ │ │ │ ├── event-diff.test.ts │ │ │ │ │ ├── sync-diagnostics.test.ts │ │ │ │ │ └── write-event-states.test.ts │ │ │ │ ├── sync/ │ │ │ │ │ ├── aggregate-runtime.test.ts │ │ │ │ │ ├── aggregate-tracker.test.ts │ │ │ │ │ └── operations.test.ts │ │ │ │ ├── sync-engine/ │ │ │ │ │ ├── index.test.ts │ │ │ │ │ └── ingest.test.ts │ │ │ │ └── utils/ │ │ │ │ └── rate-limiter.test.ts │ │ │ ├── ics/ │ │ │ │ └── utils/ │ │ │ │ ├── diff-events-extended.test.ts │ │ │ │ ├── diff-events.test.ts │ │ │ │ ├── ics-fixtures.test.ts │ │ │ │ ├── normalize-timezone.test.ts │ │ │ │ ├── outlook-windows-timezone.test.ts │ │ │ │ ├── parse-ics-calendar.test.ts │ │ │ │ └── parse-ics-events.test.ts │ │ │ ├── providers/ │ │ │ │ ├── caldav/ │ │ │ │ │ ├── destination/ │ │ │ │ │ │ └── provider.test.ts │ │ │ │ │ ├── shared/ │ │ │ │ │ │ ├── ics-fixtures.test.ts │ │ │ │ │ │ ├── ics.test.ts │ │ │ │ │ │ └── sync-window.test.ts │ │ │ │ │ └── source/ │ │ │ │ │ ├── auth-error-classification.test.ts │ │ │ │ │ ├── fetch-adapter.test.ts │ │ │ │ │ └── sync-window.test.ts │ │ │ │ ├── google/ │ │ │ │ │ ├── destination/ │ │ │ │ │ │ ├── provider.test.ts │ │ │ │ │ │ └── serialize-event.test.ts │ │ │ │ │ ├── shared/ │ │ │ │ │ │ ├── backoff.test.ts │ │ │ │ │ │ ├── batch.test.ts │ │ │ │ │ │ └── errors.test.ts │ │ │ │ │ └── source/ │ │ │ │ │ ├── fetch-adapter.test.ts │ │ │ │ │ ├── provider.test.ts │ │ │ │ │ └── utils/ │ │ │ │ │ └── fetch-events.test.ts │ │ │ │ └── outlook/ │ │ │ │ ├── destination/ │ │ │ │ │ ├── provider.test.ts │ │ │ │ │ └── serialize-event.test.ts │ │ │ │ └── source/ │ │ │ │ ├── fetch-adapter.test.ts │ │ │ │ ├── provider.test.ts │ │ │ │ └── utils/ │ │ │ │ └── fetch-events.test.ts │ │ │ └── utils/ │ │ │ └── safe-fetch.test.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── constants/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── constants/ │ │ │ │ ├── http.ts │ │ │ │ ├── scopes.ts │ │ │ │ └── time.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── data-schemas/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── client.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── database/ │ │ ├── drizzle/ │ │ │ ├── 0000_slimy_justice.sql │ │ │ ├── 0001_complete_golden_guardian.sql │ │ │ ├── 0002_striped_queen_noir.sql │ │ │ ├── 0003_nervous_vulcan.sql │ │ │ ├── 0004_strong_midnight.sql │ │ │ ├── 0005_dusty_nomad.sql │ │ │ ├── 0006_curious_orphan.sql │ │ │ ├── 0007_heavy_pretty_boy.sql │ │ │ ├── 0008_vengeful_azazel.sql │ │ │ ├── 0009_daily_thor_girl.sql │ │ │ ├── 0010_heavy_prima.sql │ │ │ ├── 0011_round_gorilla_man.sql │ │ │ ├── 0012_vengeful_thena.sql │ │ │ ├── 0013_parallel_union_jack.sql │ │ │ ├── 0014_modern_talon.sql │ │ │ ├── 0015_unique_impossible_man.sql │ │ │ ├── 0016_salty_nextwave.sql │ │ │ ├── 0017_outstanding_eddie_brock.sql │ │ │ ├── 0018_whole_loa.sql │ │ │ ├── 0019_tearful_doctor_doom.sql │ │ │ ├── 0020_huge_talon.sql │ │ │ ├── 0021_icy_white_queen.sql │ │ │ ├── 0022_lazy_avengers.sql │ │ │ ├── 0023_lyrical_genesis.sql │ │ │ ├── 0024_aberrant_wallop.sql │ │ │ ├── 0025_powerful_sentinels.sql │ │ │ ├── 0026_typical_impossible_man.sql │ │ │ ├── 0027_loose_hydra.sql │ │ │ ├── 0028_lush_sumo.sql │ │ │ ├── 0029_huge_yellow_claw.sql │ │ │ ├── 0030_youthful_speed.sql │ │ │ ├── 0031_glorious_joshua_kane.sql │ │ │ ├── 0032_dapper_patch.sql │ │ │ ├── 0033_square_tomorrow_man.sql │ │ │ ├── 0034_dumb_clanker.sql │ │ │ ├── 0035_known_silk_fever.sql │ │ │ ├── 0036_late_bastion.sql │ │ │ ├── 0037_thankful_machine_man.sql │ │ │ ├── 0038_military_radioactive_man.sql │ │ │ ├── 0039_fat_mad_thinker.sql │ │ │ ├── 0040_sparkling_toad.sql │ │ │ ├── 0041_keen_black_panther.sql │ │ │ ├── 0042_famous_obadiah_stane.sql │ │ │ ├── 0043_smart_demogoblin.sql │ │ │ ├── 0044_crazy_kate_bishop.sql │ │ │ ├── 0045_flippant_paper_doll.sql │ │ │ ├── 0046_rainy_steve_rogers.sql │ │ │ ├── 0047_soft_ravenous.sql │ │ │ ├── 0048_gigantic_kid_colt.sql │ │ │ ├── 0049_handy_sentinels.sql │ │ │ ├── 0050_purple_patch.sql │ │ │ ├── 0051_normal_mentallo.sql │ │ │ ├── 0052_military_trish_tilby.sql │ │ │ ├── 0053_greedy_reptil.sql │ │ │ ├── 0054_nasty_sage.sql │ │ │ ├── 0055_zippy_wolfsbane.sql │ │ │ ├── 0056_ambiguous_unus.sql │ │ │ ├── 0057_requeue_source_backfill.sql │ │ │ ├── 0058_same_robbie_robertson.sql │ │ │ ├── 0059_shocking_stone_men.sql │ │ │ ├── 0060_condemned_imperial_guard.sql │ │ │ ├── 0061_brief_toxin.sql │ │ │ ├── 0062_lame_white_tiger.sql │ │ │ ├── 0063_friendly_black_panther.sql │ │ │ ├── 0064_talented_black_knight.sql │ │ │ ├── 0065_dizzy_zarda.sql │ │ │ ├── 0066_nasty_bushwacker.sql │ │ │ ├── 0067_curvy_mole_man.sql │ │ │ ├── 0068_clumsy_starbolt.sql │ │ │ ├── 0069_amazing_storm.sql │ │ │ └── meta/ │ │ │ ├── 0000_snapshot.json │ │ │ ├── 0001_snapshot.json │ │ │ ├── 0002_snapshot.json │ │ │ ├── 0003_snapshot.json │ │ │ ├── 0004_snapshot.json │ │ │ ├── 0005_snapshot.json │ │ │ ├── 0006_snapshot.json │ │ │ ├── 0007_snapshot.json │ │ │ ├── 0008_snapshot.json │ │ │ ├── 0009_snapshot.json │ │ │ ├── 0010_snapshot.json │ │ │ ├── 0011_snapshot.json │ │ │ ├── 0012_snapshot.json │ │ │ ├── 0013_snapshot.json │ │ │ ├── 0014_snapshot.json │ │ │ ├── 0015_snapshot.json │ │ │ ├── 0016_snapshot.json │ │ │ ├── 0017_snapshot.json │ │ │ ├── 0018_snapshot.json │ │ │ ├── 0019_snapshot.json │ │ │ ├── 0020_snapshot.json │ │ │ ├── 0021_snapshot.json │ │ │ ├── 0022_snapshot.json │ │ │ ├── 0023_snapshot.json │ │ │ ├── 0024_snapshot.json │ │ │ ├── 0025_snapshot.json │ │ │ ├── 0026_snapshot.json │ │ │ ├── 0027_snapshot.json │ │ │ ├── 0028_snapshot.json │ │ │ ├── 0029_snapshot.json │ │ │ ├── 0030_snapshot.json │ │ │ ├── 0031_snapshot.json │ │ │ ├── 0032_snapshot.json │ │ │ ├── 0033_snapshot.json │ │ │ ├── 0034_snapshot.json │ │ │ ├── 0035_snapshot.json │ │ │ ├── 0036_snapshot.json │ │ │ ├── 0037_snapshot.json │ │ │ ├── 0038_snapshot.json │ │ │ ├── 0039_snapshot.json │ │ │ ├── 0040_snapshot.json │ │ │ ├── 0041_snapshot.json │ │ │ ├── 0042_snapshot.json │ │ │ ├── 0043_snapshot.json │ │ │ ├── 0044_snapshot.json │ │ │ ├── 0045_snapshot.json │ │ │ ├── 0046_snapshot.json │ │ │ ├── 0047_snapshot.json │ │ │ ├── 0048_snapshot.json │ │ │ ├── 0049_snapshot.json │ │ │ ├── 0050_snapshot.json │ │ │ ├── 0051_snapshot.json │ │ │ ├── 0052_snapshot.json │ │ │ ├── 0053_snapshot.json │ │ │ ├── 0054_snapshot.json │ │ │ ├── 0055_snapshot.json │ │ │ ├── 0056_snapshot.json │ │ │ ├── 0057_snapshot.json │ │ │ ├── 0058_snapshot.json │ │ │ ├── 0059_snapshot.json │ │ │ ├── 0060_snapshot.json │ │ │ ├── 0061_snapshot.json │ │ │ ├── 0062_snapshot.json │ │ │ ├── 0063_snapshot.json │ │ │ ├── 0064_snapshot.json │ │ │ ├── 0065_snapshot.json │ │ │ ├── 0066_snapshot.json │ │ │ ├── 0067_snapshot.json │ │ │ ├── 0068_snapshot.json │ │ │ ├── 0069_snapshot.json │ │ │ └── _journal.json │ │ ├── drizzle.config.ts │ │ ├── package.json │ │ ├── scripts/ │ │ │ └── migrate.ts │ │ ├── src/ │ │ │ ├── database/ │ │ │ │ ├── auth-schema.ts │ │ │ │ └── schema.ts │ │ │ ├── encryption.ts │ │ │ ├── index.ts │ │ │ └── utils/ │ │ │ └── database.ts │ │ ├── tests/ │ │ │ └── database/ │ │ │ └── auth-schema.test.ts │ │ ├── tsconfig.json │ │ ├── turbo.json │ │ └── vitest.config.ts │ ├── digest-fetch/ │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── fixtures/ │ │ ├── ics/ │ │ │ ├── berkeley-ib-seminars.ics │ │ │ ├── calendarlabs-us-holidays.ics │ │ │ ├── google-canada-holidays.ics │ │ │ ├── google-us-holidays.ics │ │ │ ├── govuk-bank-holidays-england-wales.ics │ │ │ ├── hebcal-geoname-3448439.ics │ │ │ ├── meetup-ny-tech.ics │ │ │ ├── meetup-torontojs.ics │ │ │ ├── outlook-exchange-windows-timezones.ics │ │ │ └── stanford-featured-events.ics │ │ ├── package.json │ │ ├── src/ │ │ │ ├── cache.ts │ │ │ ├── index.ts │ │ │ ├── manifest.ts │ │ │ ├── schema.ts │ │ │ └── scripts/ │ │ │ ├── sync-fixtures.ts │ │ │ └── verify-fixtures.ts │ │ └── tsconfig.json │ ├── otelemetry/ │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── premium/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ └── subscription.ts │ │ └── tsconfig.json │ ├── queue/ │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── sync/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── destination-errors.ts │ │ │ ├── index.ts │ │ │ ├── resolve-provider.ts │ │ │ ├── sync-lock.ts │ │ │ └── sync-user.ts │ │ ├── tests/ │ │ │ ├── destination-errors.test.ts │ │ │ └── sync-lock.test.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ └── typescript-config/ │ ├── package.json │ └── tsconfig.json ├── scripts/ │ └── bun-test.ts ├── services/ │ ├── api/ │ │ ├── Dockerfile │ │ ├── entrypoint.sh │ │ ├── package.json │ │ ├── scripts/ │ │ │ └── build.ts │ │ ├── src/ │ │ │ ├── context.ts │ │ │ ├── env.ts │ │ │ ├── handlers/ │ │ │ │ ├── auth-oauth-resource.ts │ │ │ │ ├── auth.ts │ │ │ │ ├── websocket-initial-status.ts │ │ │ │ ├── websocket-payload.ts │ │ │ │ └── websocket.ts │ │ │ ├── index.ts │ │ │ ├── middleware/ │ │ │ │ └── cors.ts │ │ │ ├── mutations/ │ │ │ │ ├── index.ts │ │ │ │ ├── providers/ │ │ │ │ │ ├── caldav.ts │ │ │ │ │ ├── google.ts │ │ │ │ │ └── outlook.ts │ │ │ │ └── resolve-credentials.ts │ │ │ ├── provider-display.ts │ │ │ ├── queries/ │ │ │ │ ├── get-event-count.ts │ │ │ │ ├── get-event.ts │ │ │ │ ├── get-events-in-range.ts │ │ │ │ ├── get-sync-statuses.ts │ │ │ │ ├── list-destinations.ts │ │ │ │ ├── list-mappings.ts │ │ │ │ └── list-sources.ts │ │ │ ├── read-models.ts │ │ │ ├── routes/ │ │ │ │ └── api/ │ │ │ │ ├── accounts/ │ │ │ │ │ ├── [id].ts │ │ │ │ │ └── index.ts │ │ │ │ ├── cal/ │ │ │ │ │ └── [identifier].ts │ │ │ │ ├── destinations/ │ │ │ │ │ ├── [id].ts │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── caldav/ │ │ │ │ │ │ ├── discover.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── callback/ │ │ │ │ │ │ └── [provider].ts │ │ │ │ │ └── index.ts │ │ │ │ ├── entitlements.ts │ │ │ │ ├── events/ │ │ │ │ │ ├── count.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── feedback/ │ │ │ │ │ └── index.ts │ │ │ │ ├── health.ts │ │ │ │ ├── ical/ │ │ │ │ │ ├── settings.ts │ │ │ │ │ └── token.ts │ │ │ │ ├── ics/ │ │ │ │ │ ├── [id]/ │ │ │ │ │ │ └── destinations.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── source-routes.ts │ │ │ │ ├── mappings/ │ │ │ │ │ └── index.ts │ │ │ │ ├── socket/ │ │ │ │ │ ├── token.ts │ │ │ │ │ └── url.ts │ │ │ │ ├── sources/ │ │ │ │ │ ├── [id]/ │ │ │ │ │ │ ├── destinations.ts │ │ │ │ │ │ ├── mapping-routes.ts │ │ │ │ │ │ ├── source-item-routes.ts │ │ │ │ │ │ └── sources.ts │ │ │ │ │ ├── [id].ts │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── caldav/ │ │ │ │ │ │ ├── discover.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── callback/ │ │ │ │ │ │ └── [provider].ts │ │ │ │ │ ├── callback-state.ts │ │ │ │ │ ├── google/ │ │ │ │ │ │ ├── [id]/ │ │ │ │ │ │ │ └── destinations.ts │ │ │ │ │ │ ├── calendars.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── outlook/ │ │ │ │ │ ├── [id]/ │ │ │ │ │ │ └── destinations.ts │ │ │ │ │ ├── calendars.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── sync/ │ │ │ │ │ └── status.ts │ │ │ │ ├── tokens/ │ │ │ │ │ ├── [id].ts │ │ │ │ │ └── index.ts │ │ │ │ ├── v1/ │ │ │ │ │ ├── accounts/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── calendars/ │ │ │ │ │ │ ├── [calendarId]/ │ │ │ │ │ │ │ └── invites.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── events/ │ │ │ │ │ │ ├── [id].ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── ical/ │ │ │ │ │ └── index.ts │ │ │ │ └── webhook/ │ │ │ │ └── polar.ts │ │ │ ├── types.ts │ │ │ └── utils/ │ │ │ ├── api-rate-limit.ts │ │ │ ├── api-tokens.ts │ │ │ ├── background-task.ts │ │ │ ├── caldav-sources.ts │ │ │ ├── caldav.ts │ │ │ ├── date-range.ts │ │ │ ├── destinations.ts │ │ │ ├── enqueue-push-sync.ts │ │ │ ├── ical-format.ts │ │ │ ├── ical.ts │ │ │ ├── invalidate-calendars.ts │ │ │ ├── logging.ts │ │ │ ├── middleware.ts │ │ │ ├── oauth-calendar-listing.ts │ │ │ ├── oauth-callback-state.ts │ │ │ ├── oauth-refresh.ts │ │ │ ├── oauth-source-credentials.ts │ │ │ ├── oauth-sources.ts │ │ │ ├── oauth.ts │ │ │ ├── provider-display.ts │ │ │ ├── request-body.ts │ │ │ ├── request-query.ts │ │ │ ├── responses.ts │ │ │ ├── route-handler.ts │ │ │ ├── safe-fetch-options.ts │ │ │ ├── source-destination-mappings.ts │ │ │ ├── source-lifecycle.ts │ │ │ ├── source-sync-defaults.ts │ │ │ ├── sources.ts │ │ │ ├── state.ts │ │ │ └── user.ts │ │ ├── tests/ │ │ │ ├── handlers/ │ │ │ │ ├── auth-oauth-resource.test.ts │ │ │ │ ├── auth.test.ts │ │ │ │ ├── websocket-initial-status.test.ts │ │ │ │ └── websocket-payload.test.ts │ │ │ ├── routes/ │ │ │ │ └── api/ │ │ │ │ ├── ical/ │ │ │ │ │ └── settings.test.ts │ │ │ │ ├── ics/ │ │ │ │ │ └── source-routes.test.ts │ │ │ │ └── sources/ │ │ │ │ └── [id]/ │ │ │ │ ├── mapping-routes.test.ts │ │ │ │ └── source-item-routes.test.ts │ │ │ └── utils/ │ │ │ ├── account-locks.test.ts │ │ │ ├── api-rate-limit.test.ts │ │ │ ├── enqueue-push-sync.test.ts │ │ │ ├── ical.test.ts │ │ │ ├── oauth-sources.test.ts │ │ │ ├── oauth.test.ts │ │ │ ├── request-body.test.ts │ │ │ ├── source-destination-mappings.test.ts │ │ │ ├── source-lifecycle.test.ts │ │ │ └── source-sync-defaults.test.ts │ │ ├── tsconfig.json │ │ ├── turbo.json │ │ └── vitest.config.ts │ ├── cron/ │ │ ├── Dockerfile │ │ ├── entrypoint.sh │ │ ├── package.json │ │ ├── scripts/ │ │ │ └── build.ts │ │ ├── src/ │ │ │ ├── context.ts │ │ │ ├── env.ts │ │ │ ├── index.ts │ │ │ ├── jobs/ │ │ │ │ ├── ingest-sources.ts │ │ │ │ ├── push-destinations.ts │ │ │ │ └── reconcile-subscriptions.ts │ │ │ ├── migration-check.ts │ │ │ └── utils/ │ │ │ ├── baker.ts │ │ │ ├── get-jobs.ts │ │ │ ├── get-sources.ts │ │ │ ├── inject-jobs.ts │ │ │ ├── logging.ts │ │ │ ├── register-jobs.ts │ │ │ ├── safe-fetch-options.ts │ │ │ ├── source-plan-selection.ts │ │ │ └── with-wide-event.ts │ │ ├── tests/ │ │ │ ├── jobs/ │ │ │ │ └── reconcile-subscriptions.test.ts │ │ │ ├── migration-check.test.ts │ │ │ └── utils/ │ │ │ ├── get-jobs.test.ts │ │ │ └── source-plan-selection.test.ts │ │ ├── tsconfig.json │ │ ├── turbo.json │ │ └── vitest.config.ts │ ├── mcp/ │ │ ├── Dockerfile │ │ ├── entrypoint.sh │ │ ├── package.json │ │ ├── scripts/ │ │ │ └── build.ts │ │ ├── src/ │ │ │ ├── context.ts │ │ │ ├── env.ts │ │ │ ├── index.ts │ │ │ ├── mcp-handler.ts │ │ │ ├── routes/ │ │ │ │ ├── health.ts │ │ │ │ └── mcp.ts │ │ │ ├── toolset.ts │ │ │ └── utils/ │ │ │ ├── logging.ts │ │ │ ├── middleware.ts │ │ │ └── route-handler.ts │ │ ├── tests/ │ │ │ ├── mcp-handler.test.ts │ │ │ └── toolset.test.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ └── worker/ │ ├── Dockerfile │ ├── entrypoint.sh │ ├── package.json │ ├── scripts/ │ │ └── build.ts │ ├── src/ │ │ ├── context.ts │ │ ├── env.ts │ │ ├── index.ts │ │ ├── processor.ts │ │ └── utils/ │ │ └── logging.ts │ └── tsconfig.json └── turbo.json
SYMBOL INDEX (1969 symbols across 435 files)
FILE: applications/web/plugins/blog.ts
type BlogPostMetadata (line 18) | type BlogPostMetadata = typeof blogPostMetadataSchema.infer;
type ProcessedBlogPost (line 20) | interface ProcessedBlogPost {
function toIsoDate (line 26) | function toIsoDate(value: string): string {
function normalizeMetadataInput (line 30) | function normalizeMetadataInput(value: unknown): unknown {
function splitFrontmatter (line 44) | function splitFrontmatter(
function parseMetadata (line 72) | function parseMetadata(value: unknown, filePath: string): BlogPostMetada...
function createSlug (line 93) | function createSlug(title: string): string {
function removeRedundantLeadingHeading (line 102) | function removeRedundantLeadingHeading(
function processBlogDirectory (line 122) | function processBlogDirectory(blogDir: string): ProcessedBlogPost[] {
constant VIRTUAL_MODULE_ID (line 155) | const VIRTUAL_MODULE_ID = "virtual:blog-posts";
constant RESOLVED_ID (line 156) | const RESOLVED_ID = `\0${VIRTUAL_MODULE_ID}`;
function blogPlugin (line 158) | function blogPlugin(): Plugin {
FILE: applications/web/plugins/sitemap.ts
constant SITE_URL (line 7) | const SITE_URL = "https://keeper.sh";
constant FRONTMATTER_PATTERN (line 8) | const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
type SitemapEntry (line 10) | interface SitemapEntry {
function parseFrontmatter (line 22) | function parseFrontmatter(raw: string): Record<string, unknown> {
function discoverBlogEntries (line 28) | function discoverBlogEntries(blogDir: string): SitemapEntry[] {
function buildSitemapXml (line 56) | function buildSitemapXml(entries: SitemapEntry[]): string {
function sitemapPlugin (line 71) | function sitemapPlugin(): Plugin {
FILE: applications/web/src/components/analytics-scripts.tsx
function resolveConsentLabel (line 18) | function resolveConsentLabel(hasConsent: boolean): "granted" | "denied" {
function AnalyticsScripts (line 23) | function AnalyticsScripts({ runtimeConfig }: { runtimeConfig: PublicRunt...
FILE: applications/web/src/components/cookie-consent.tsx
constant CARD_ENTER (line 11) | const CARD_ENTER = { opacity: 0, y: 10, filter: "blur(4px)" };
constant CARD_VISIBLE (line 12) | const CARD_VISIBLE = { opacity: 1, y: 0, filter: "blur(0px)" };
constant CARD_EXIT (line 13) | const CARD_EXIT = { opacity: 0, y: 10, filter: "blur(4px)" };
constant COLLAPSE_ANIMATE (line 14) | const COLLAPSE_ANIMATE = { height: "auto" };
constant COLLAPSE_EXIT (line 15) | const COLLAPSE_EXIT = { height: 0 };
constant CARD_TRANSITION (line 16) | const CARD_TRANSITION = { duration: 0.2 };
constant COLLAPSE_TRANSITION (line 17) | const COLLAPSE_TRANSITION = { duration: 0.2, delay: 0.15 };
function resolveConsentEventName (line 19) | function resolveConsentEventName(consent: boolean): string {
function ConsentBannerContent (line 24) | function ConsentBannerContent({ children }: PropsWithChildren) {
function ConsentBannerActions (line 32) | function ConsentBannerActions({ children }: PropsWithChildren) {
function ConsentBannerCard (line 40) | function ConsentBannerCard({ children }: PropsWithChildren) {
function CookieConsent (line 48) | function CookieConsent() {
FILE: applications/web/src/components/ui/composites/navigation-menu/navigation-menu-editable.tsx
type NavigationMenuEditableItemProps (line 14) | type NavigationMenuEditableItemProps = {
function NavigationMenuEditableItem (line 23) | function NavigationMenuEditableItem(props: NavigationMenuEditableItemPro...
type NavigationMenuEditableTemplateItemProps (line 67) | type NavigationMenuEditableTemplateItemProps = {
function NavigationMenuEditableTemplateItem (line 78) | function NavigationMenuEditableTemplateItem(props: NavigationMenuEditabl...
function useEditableCommit (line 125) | function useEditableCommit(
function EditableItemInput (line 176) | function EditableItemInput({
function EditableTemplateItemInput (line 206) | function EditableTemplateItemInput({
function TemplateInputOverlay (line 252) | function TemplateInputOverlay({
function EditableItemDefaultValue (line 296) | function EditableItemDefaultValue({ value, label }: { value: ReactNode; ...
function EditableItemDisplay (line 311) | function EditableItemDisplay({
FILE: applications/web/src/components/ui/composites/navigation-menu/navigation-menu-items.tsx
type NavigationMenuProps (line 25) | type NavigationMenuProps = PropsWithChildren<{
function NavigationMenu (line 30) | function NavigationMenu({
type NavigationMenuItemProps (line 42) | type NavigationMenuItemProps = PropsWithChildren<{
type NavigationMenuLinkItemProps (line 46) | type NavigationMenuLinkItemProps = PropsWithChildren<{
type NavigationMenuButtonItemProps (line 53) | type NavigationMenuButtonItemProps = PropsWithChildren<{
type NavigationMenuItemLabelProps (line 59) | type NavigationMenuItemLabelProps = PropsWithChildren<{
type NavigationMenuItemTrailingProps (line 63) | type NavigationMenuItemTrailingProps = PropsWithChildren<{
function NavigationMenuItem (line 67) | function NavigationMenuItem({
function NavigationMenuLinkItem (line 84) | function NavigationMenuLinkItem({
function NavigationMenuButtonItem (line 120) | function NavigationMenuButtonItem({
function NavigationMenuItemIcon (line 144) | function NavigationMenuItemIcon({ children }: PropsWithChildren) {
function NavigationMenuItemLabel (line 151) | function NavigationMenuItemLabel({
function NavigationMenuEmptyItem (line 171) | function NavigationMenuEmptyItem({ children }: PropsWithChildren) {
function NavigationMenuItemTrailing (line 190) | function NavigationMenuItemTrailing({
type NavigationMenuToggleableItemProps (line 210) | type NavigationMenuToggleableItemProps = PropsWithChildren<{
function NavigationMenuCheckboxItem (line 217) | function NavigationMenuCheckboxItem({
function NavigationMenuToggleItem (line 245) | function NavigationMenuToggleItem({
FILE: applications/web/src/components/ui/composites/navigation-menu/navigation-menu-popover.tsx
constant POPOVER_INITIAL (line 22) | const POPOVER_INITIAL = { opacity: 1 } as const;
constant SHADOW_HIDDEN (line 23) | const SHADOW_HIDDEN = { boxShadow: "0 0 0 0 rgba(0,0,0,0)" } as const;
constant SHADOW_VISIBLE (line 24) | const SHADOW_VISIBLE = {
constant TRIGGER_INITIAL (line 27) | const TRIGGER_INITIAL = { height: "fit-content" as const, filter: "blur(...
constant TRIGGER_ANIMATE (line 28) | const TRIGGER_ANIMATE = { height: 0, filter: "blur(0)", opacity: 0 };
constant TRIGGER_EXIT (line 29) | const TRIGGER_EXIT = { height: "fit-content" as const, filter: "blur(0)"...
constant CONTENT_INITIAL (line 30) | const CONTENT_INITIAL = { height: 0, filter: "blur(0)", opacity: 0 };
constant CONTENT_ANIMATE (line 31) | const CONTENT_ANIMATE = { height: "fit-content" as const, filter: "blur(...
constant CONTENT_EXIT (line 32) | const CONTENT_EXIT = { height: 0, filter: "blur(4px)", opacity: 0 };
constant POPOVER_CONTENT_STYLE (line 33) | const POPOVER_CONTENT_STYLE = { maxHeight: "16rem" } as const;
type NavigationMenuPopoverProps (line 35) | type NavigationMenuPopoverProps = {
function NavigationMenuPopover (line 41) | function NavigationMenuPopover({
function NavigationMenuPopoverPanel (line 142) | function NavigationMenuPopoverPanel({ children }: PropsWithChildren) {
FILE: applications/web/src/components/ui/composites/navigation-menu/navigation-menu.contexts.ts
type PopoverContextValue (line 9) | type PopoverContextValue = {
function usePopover (line 18) | function usePopover() {
FILE: applications/web/src/components/ui/composites/navigation-menu/navigation-menu.styles.ts
type MenuVariant (line 16) | type MenuVariant = VariantProps<typeof navigationMenuStyle>["variant"];
constant LABEL_TONE (line 139) | const LABEL_TONE: Record<NonNullable<MenuVariant>, "muted" | "inverse" |...
constant DISABLED_LABEL_TONE (line 144) | const DISABLED_LABEL_TONE: Record<
FILE: applications/web/src/components/ui/primitives/animated-reveal.tsx
constant HIDDEN (line 6) | const HIDDEN = { height: 0, opacity: 0, filter: "blur(4px)" };
constant VISIBLE (line 7) | const VISIBLE = { height: "fit-content", opacity: 1, filter: "blur(0)" };
constant CLIP_STYLE (line 8) | const CLIP_STYLE = { overflow: "clip" as const, overflowClipMargin: 4 };
type AnimatedRevealProps (line 10) | interface AnimatedRevealProps {
function AnimatedReveal (line 16) | function AnimatedReveal({ show, skipInitial, children }: AnimatedRevealP...
FILE: applications/web/src/components/ui/primitives/back-button.tsx
type BackButtonProps (line 5) | interface BackButtonProps {
function BackButton (line 12) | function BackButton({
FILE: applications/web/src/components/ui/primitives/button.tsx
type ButtonProps (line 28) | type ButtonProps = VariantProps<typeof button>;
type ButtonOptions (line 29) | type ButtonOptions = ComponentPropsWithoutRef<"button"> & ButtonProps;
type LinkButtonOptions (line 30) | type LinkButtonOptions = Omit<ComponentPropsWithoutRef<typeof Link>, "ch...
type ExternalLinkButtonOptions (line 32) | type ExternalLinkButtonOptions = ComponentPropsWithoutRef<"a"> & Variant...
function Button (line 34) | function Button({ children, size, variant, className, ...props }: Button...
function LinkButton (line 40) | function LinkButton({ children, size, variant, className, ...props }: Li...
function ExternalLinkButton (line 46) | function ExternalLinkButton({ children, size, variant, className, ...pro...
function ButtonText (line 52) | function ButtonText({ children }: PropsWithChildren) {
function ButtonIcon (line 56) | function ButtonIcon({ children }: PropsWithChildren) {
FILE: applications/web/src/components/ui/primitives/checkbox.tsx
type CheckboxVariant (line 42) | type CheckboxVariant = "default" | "highlight";
type CheckboxIndicatorProps (line 44) | interface CheckboxIndicatorProps {
function CheckboxIndicator (line 50) | function CheckboxIndicator({ checked, variant, className }: CheckboxIndi...
type CheckboxProps (line 58) | interface CheckboxProps {
function Checkbox (line 65) | function Checkbox({ checked, onCheckedChange, children, className }: Che...
FILE: applications/web/src/components/ui/primitives/collapsible.tsx
type CollapsibleProps (line 20) | type CollapsibleProps = PropsWithChildren<{
function Collapsible (line 25) | function Collapsible({ trigger, children, className }: CollapsibleProps) {
FILE: applications/web/src/components/ui/primitives/dashboard-heading.tsx
type HeadingLevel (line 16) | type HeadingLevel = 1 | 2 | 3;
type HeadingTag (line 17) | type HeadingTag = "h1" | "h2" | "h3" | "span" | "p";
type DashboardHeadingProps (line 18) | type DashboardHeadingProps = PropsWithChildren<{ level: HeadingLevel; as...
function DashboardHeadingBase (line 22) | function DashboardHeadingBase({ children, level, as, className }: Dashbo...
function DashboardHeading1 (line 27) | function DashboardHeading1({ children, as, className }: Omit<DashboardHe...
function DashboardHeading2 (line 31) | function DashboardHeading2({ children, as, className }: Omit<DashboardHe...
function DashboardHeading3 (line 35) | function DashboardHeading3({ children, as, className }: Omit<DashboardHe...
type DashboardSectionProps (line 39) | type DashboardSectionProps = {
function DashboardSection (line 46) | function DashboardSection({ title, description, level = 2, headingClassN...
FILE: applications/web/src/components/ui/primitives/delete-confirmation.tsx
type DeleteConfirmationProps (line 11) | interface DeleteConfirmationProps {
function resolveDeleteLabel (line 20) | function resolveDeleteLabel(deleting: boolean): string {
function DeleteConfirmation (line 25) | function DeleteConfirmation({
FILE: applications/web/src/components/ui/primitives/divider.tsx
function Divider (line 5) | function Divider({ children }: PropsWithChildren) {
FILE: applications/web/src/components/ui/primitives/error-state.tsx
type ErrorStateProps (line 4) | interface ErrorStateProps {
function ErrorState (line 9) | function ErrorState({
FILE: applications/web/src/components/ui/primitives/fade-in.tsx
type Direction (line 6) | type Direction = "from-right" | "from-top" | "from-bottom";
constant TRANSITION (line 23) | const TRANSITION = { duration: 0.2 } as const;
type FadeInProps (line 25) | interface FadeInProps extends HTMLMotionProps<"div"> {
function FadeIn (line 29) | function FadeIn({ direction, children, ...props }: PropsWithChildren<Fad...
FILE: applications/web/src/components/ui/primitives/github-star-button.tsx
constant SCROLL_THRESHOLD (line 13) | const SCROLL_THRESHOLD = 32;
constant GITHUB_STARS_ENDPOINT_PATH (line 14) | const GITHUB_STARS_ENDPOINT_PATH = "/internal/github-stars";
constant GITHUB_REPOSITORY_URL (line 15) | const GITHUB_REPOSITORY_URL = "https://github.com/ridafkih/keeper.sh";
type GithubStarsResponse (line 17) | interface GithubStarsResponse {
function isGithubStarsResponse (line 22) | function isGithubStarsResponse(value: unknown): value is GithubStarsResp...
function formatStarCount (line 34) | function formatStarCount(starCount: number): string {
function fetchGithubStarCount (line 42) | async function fetchGithubStarCount(url: string): Promise<number> {
type GithubStarButtonProps (line 59) | interface GithubStarButtonProps {
type GithubStarButtonShellProps (line 63) | interface GithubStarButtonShellProps {
type GithubStarErrorBoundaryState (line 67) | interface GithubStarErrorBoundaryState {
class GithubStarErrorBoundary (line 71) | class GithubStarErrorBoundary extends Component<
method getDerivedStateFromError (line 79) | static getDerivedStateFromError(): GithubStarErrorBoundaryState {
method render (line 85) | render() {
function GithubStarButtonShell (line 94) | function GithubStarButtonShell({ countLabel }: GithubStarButtonShellProp...
function GithubStarButtonCount (line 110) | function GithubStarButtonCount({ initialStarCount }: GithubStarButtonPro...
function GithubStarButton (line 132) | function GithubStarButton({ initialStarCount }: GithubStarButtonProps) {
FILE: applications/web/src/components/ui/primitives/heading.tsx
type HeadingLevel (line 15) | type HeadingLevel = 1 | 2 | 3;
type HeadingTag (line 16) | type HeadingTag = "h1" | "h2" | "h3" | "span" | "p";
type HeadingProps (line 17) | type HeadingProps = PropsWithChildren<{ level: HeadingLevel; as?: Headin...
function HeadingBase (line 21) | function HeadingBase({ children, level, as, className }: HeadingProps) {
function Heading1 (line 26) | function Heading1({ children, as, className }: Omit<HeadingProps, "level...
function Heading2 (line 30) | function Heading2({ children, as, className }: Omit<HeadingProps, "level...
function Heading3 (line 34) | function Heading3({ children, as, className }: Omit<HeadingProps, "level...
FILE: applications/web/src/components/ui/primitives/input.tsx
type InputProps (line 17) | type InputProps = ComponentPropsWithoutRef<"input"> & VariantProps<typeo...
function Input (line 21) | function Input({ tone, className, ref, ...props }: InputProps) {
FILE: applications/web/src/components/ui/primitives/list.tsx
type ListProps (line 3) | type ListProps = PropsWithChildren<ComponentPropsWithoutRef<"ul">>;
type OrderedListProps (line 4) | type OrderedListProps = PropsWithChildren<ComponentPropsWithoutRef<"ol">>;
type ListItemProps (line 5) | type ListItemProps = PropsWithChildren<ComponentPropsWithoutRef<"li">>;
function UnorderedList (line 7) | function UnorderedList({ children, className, ...props }: ListProps) {
function OrderedList (line 21) | function OrderedList({ children, className, ...props }: OrderedListProps) {
function ListItem (line 35) | function ListItem({ children, className, ...props }: ListItemProps) {
FILE: applications/web/src/components/ui/primitives/markdown-components.tsx
type MarkdownElementProps (line 6) | type MarkdownElementProps<Tag extends keyof JSX.IntrinsicElements> =
function isExternalHttpLink (line 11) | function isExternalHttpLink(href: string): boolean {
function MarkdownHeadingOne (line 15) | function MarkdownHeadingOne({ children }: MarkdownElementProps<"h1">) {
function MarkdownHeadingTwo (line 19) | function MarkdownHeadingTwo({ children }: MarkdownElementProps<"h2">) {
function MarkdownHeadingThree (line 23) | function MarkdownHeadingThree({ children }: MarkdownElementProps<"h3">) {
function MarkdownParagraph (line 27) | function MarkdownParagraph({ children }: MarkdownElementProps<"p">) {
function MarkdownLink (line 35) | function MarkdownLink({
function MarkdownUnorderedList (line 56) | function MarkdownUnorderedList({
function MarkdownOrderedList (line 62) | function MarkdownOrderedList({
function MarkdownListItem (line 68) | function MarkdownListItem({ children }: MarkdownElementProps<"li">) {
function MarkdownInlineCode (line 72) | function MarkdownInlineCode({ children }: MarkdownElementProps<"code">) {
function MarkdownCodeBlock (line 80) | function MarkdownCodeBlock({
function MarkdownBlockquote (line 90) | function MarkdownBlockquote({ children }: MarkdownElementProps<"blockquo...
function MarkdownRule (line 100) | function MarkdownRule() {
function MarkdownTable (line 104) | function MarkdownTable({
function MarkdownTableHeader (line 116) | function MarkdownTableHeader({
function MarkdownTableCell (line 122) | function MarkdownTableCell({
FILE: applications/web/src/components/ui/primitives/modal.tsx
type ModalContextValue (line 9) | interface ModalContextValue {
function useModal (line 16) | function useModal() {
type ModalProps (line 22) | interface ModalProps extends PropsWithChildren {
function Modal (line 27) | function Modal({ children, open: controlledOpen, onOpenChange }: ModalPr...
function ModalContent (line 42) | function ModalContent({ children }: PropsWithChildren) {
function ModalTitle (line 83) | function ModalTitle({ children }: PropsWithChildren) {
function ModalDescription (line 87) | function ModalDescription({ children }: PropsWithChildren) {
function ModalFooter (line 91) | function ModalFooter({ children }: PropsWithChildren) {
FILE: applications/web/src/components/ui/primitives/pagination.tsx
function Pagination (line 6) | function Pagination({ children }: PropsWithChildren) {
function PaginationPrevious (line 10) | function PaginationPrevious({ to, onMouseEnter }: { to?: string; onMouse...
function PaginationNext (line 26) | function PaginationNext({ to, onMouseEnter }: { to?: string; onMouseEnte...
FILE: applications/web/src/components/ui/primitives/provider-icon-stack.tsx
type ProviderIconStackProps (line 7) | interface ProviderIconStackProps {
function ProviderIconStackItem (line 13) | function ProviderIconStackItem({ provider, calendarType }: { provider?: ...
constant HIDDEN (line 21) | const HIDDEN = { opacity: 0, filter: "blur(4px)", width: 0 };
constant VISIBLE (line 22) | const VISIBLE = { opacity: 1, filter: "blur(0)", width: "auto" };
function resolveInitial (line 24) | function resolveInitial(animate: boolean) {
function ProviderIconStack (line 29) | function ProviderIconStack({ providers, max = 4, animate = false }: Prov...
FILE: applications/web/src/components/ui/primitives/provider-icon.tsx
type ProviderIconProps (line 5) | interface ProviderIconProps {
function resolveIconPath (line 11) | function resolveIconPath(provider: string | undefined): string | undefin...
function ProviderIcon (line 16) | function ProviderIcon({ provider, calendarType, size = 15 }: ProviderIco...
FILE: applications/web/src/components/ui/primitives/shimmer-text.tsx
type ShimmerTextProps (line 4) | type ShimmerTextProps = PropsWithChildren<{
function ShimmerText (line 8) | function ShimmerText({ children, className }: ShimmerTextProps) {
FILE: applications/web/src/components/ui/primitives/staggered-backdrop-blur.tsx
constant LAYERS (line 1) | const LAYERS = [
function StaggeredBackdropBlur (line 12) | function StaggeredBackdropBlur() {
FILE: applications/web/src/components/ui/primitives/template-text.tsx
type TemplateTextProps (line 17) | interface TemplateTextProps {
function TemplateText (line 24) | function TemplateText({ template, variables, disabled, className }: Temp...
FILE: applications/web/src/components/ui/primitives/text-link.tsx
type TextLinkProps (line 29) | type TextLinkProps = Omit<ComponentPropsWithoutRef<typeof Link>, "childr...
type ExternalTextLinkProps (line 31) | type ExternalTextLinkProps = ComponentPropsWithoutRef<"a"> &
function TextLink (line 34) | function TextLink({ children, size, tone, align, className, ...props }: ...
function ExternalTextLink (line 42) | function ExternalTextLink({
FILE: applications/web/src/components/ui/primitives/text.tsx
type TextProps (line 34) | type TextProps = PropsWithChildren<{
function Text (line 43) | function Text({ as = "p", children, size, tone, align, className, style ...
FILE: applications/web/src/components/ui/primitives/tooltip.tsx
constant GAP (line 5) | const GAP = 4;
constant ABOVE_CLEARANCE (line 6) | const ABOVE_CLEARANCE = 32;
type TooltipProps (line 8) | type TooltipProps = PropsWithChildren<{
function Tooltip (line 12) | function Tooltip({ children, content }: TooltipProps) {
FILE: applications/web/src/components/ui/primitives/upgrade-hint.tsx
function UpgradeHint (line 6) | function UpgradeHint({ children }: PropsWithChildren) {
function PremiumFeatureGate (line 19) | function PremiumFeatureGate({ locked, children, hint }: { locked: boolea...
FILE: applications/web/src/components/ui/shells/layout.tsx
constant GRID_COLS (line 4) | const GRID_COLS = "grid grid-cols-[minmax(1rem,1fr)_minmax(auto,48rem)_m...
function Layout (line 6) | function Layout({ children }: PropsWithChildren) {
function LayoutItem (line 14) | function LayoutItem({ children }: PropsWithChildren) {
function LayoutRow (line 20) | function LayoutRow({ children, className }: PropsWithChildren<{ classNam...
FILE: applications/web/src/components/ui/shells/route-shell.tsx
type RouteShellProps (line 5) | type RouteShellProps = {
function RouteShell (line 13) | function RouteShell(props: RouteShellProps) {
FILE: applications/web/src/components/ui/shells/session-slot.tsx
type SessionSlotProps (line 5) | interface SessionSlotProps {
function SessionSlot (line 13) | function SessionSlot({ authenticated, unauthenticated }: SessionSlotProp...
FILE: applications/web/src/config/gdpr.ts
constant GDPR_COUNTRIES (line 2) | const GDPR_COUNTRIES = new Set([
FILE: applications/web/src/config/plans.ts
type PlanConfig (line 3) | interface PlanConfig {
FILE: applications/web/src/features/auth/components/auth-form.tsx
function resolveInputTone (line 44) | function resolveInputTone(active: boolean | undefined): "error" | "neutr...
function resolveSwitchSearch (line 49) | function resolveSwitchSearch(search?: StringSearchParams): StringSearchP...
type AuthScreenCopy (line 56) | type AuthScreenCopy = {
type SocialAuthProvider (line 67) | type SocialAuthProvider = {
constant SOCIAL_AUTH_PROVIDERS (line 74) | const SOCIAL_AUTH_PROVIDERS: readonly SocialAuthProvider[] = [
function resolvePasswordFieldAnimation (line 89) | function resolvePasswordFieldAnimation(step: "email" | "password"): Targ...
function AuthForm (line 97) | function AuthForm({
function redirectAfterAuth (line 148) | function redirectAfterAuth(authorizationSearch?: StringSearchParams) {
function usePasskeyAutoFill (line 158) | function usePasskeyAutoFill(authorizationSearch?: StringSearchParams) {
type PasskeyAutoFillProps (line 194) | interface PasskeyAutoFillProps {
function PasskeyAutoFill (line 198) | function PasskeyAutoFill({ authorizationSearch }: PasskeyAutoFillProps) {
function SocialAuthButtons (line 203) | function SocialAuthButtons({
function FormBackButton (line 239) | function FormBackButton({ step, onBack }: { step: "email" | "password"; ...
function ForgotPasswordLink (line 244) | function ForgotPasswordLink({
function resolveAutoComplete (line 259) | function resolveAutoComplete(
function readFormFieldValue (line 272) | function readFormFieldValue(formData: FormData, fieldName: string): stri...
function CredentialForm (line 278) | function CredentialForm({
function resolveAuthErrorAnimation (line 398) | function resolveAuthErrorAnimation(active: boolean | undefined): TargetA...
function AuthError (line 403) | function AuthError() {
function CredentialInput (line 423) | function CredentialInput({
function PasswordInput (line 470) | function PasswordInput({
function AnimatedBackWrapper (line 506) | function AnimatedBackWrapper({ children }: { children: React.ReactNode }) {
function BackButton (line 529) | function BackButton() {
function StepBackButton (line 541) | function StepBackButton({ onBack }: { onBack: () => void }) {
function SubmitButton (line 553) | function SubmitButton({ children }: { children: string }) {
FILE: applications/web/src/features/auth/components/auth-switch-prompt.tsx
function AuthSwitchPrompt (line 4) | function AuthSwitchPrompt({ children }: PropsWithChildren) {
FILE: applications/web/src/features/auth/components/caldav-connect-form.tsx
type CalDAVProvider (line 14) | type CalDAVProvider = "fastmail" | "icloud" | "caldav";
type ProviderConfig (line 16) | interface ProviderConfig {
constant PROVIDER_CONFIGS (line 24) | const PROVIDER_CONFIGS: Record<CalDAVProvider, ProviderConfig> = {
type CalendarOption (line 48) | interface CalendarOption {
type CalDAVConnectFormProps (line 53) | interface CalDAVConnectFormProps {
function readFormFieldValue (line 57) | function readFormFieldValue(formData: FormData, fieldName: string): stri...
function isRecord (line 63) | function isRecord(value: unknown): value is Record<string, unknown> {
function isCalendarOption (line 67) | function isCalendarOption(value: unknown): value is CalendarOption {
function parseCalendarOptions (line 72) | function parseCalendarOptions(value: unknown): CalendarOption[] | null {
function parseAuthMethod (line 80) | function parseAuthMethod(value: unknown): "basic" | "digest" {
function parseAccountId (line 86) | function parseAccountId(value: unknown): string | undefined {
function CalDAVConnectForm (line 92) | function CalDAVConnectForm({ provider }: CalDAVConnectFormProps) {
FILE: applications/web/src/features/auth/components/caldav-connect-page.tsx
type CalDAVConnectPageProps (line 7) | interface CalDAVConnectPageProps {
function CalDAVConnectPage (line 16) | function CalDAVConnectPage({
FILE: applications/web/src/features/auth/components/ics-connect-form.tsx
function resolveSubmitLabel (line 14) | function resolveSubmitLabel(pending: boolean): string {
function ICSFeedForm (line 19) | function ICSFeedForm() {
FILE: applications/web/src/features/auth/components/oauth-preamble.tsx
type Provider (line 18) | type Provider = "google" | "outlook" | "microsoft-365";
constant PROVIDER_LABELS (line 20) | const PROVIDER_LABELS: Record<Provider, string> = {
constant PERMISSIONS (line 26) | const PERMISSIONS = [
constant PROVIDER_SOCIAL_MAP (line 33) | const PROVIDER_SOCIAL_MAP: Partial<Record<Provider, string>> = {
constant PROVIDER_API_MAP (line 38) | const PROVIDER_API_MAP: Record<Provider, string> = {
function PermissionsList (line 44) | function PermissionsList({ items }: { items: readonly string[] }) {
type PreambleLayoutProps (line 57) | interface PreambleLayoutProps {
function PreambleLayout (line 63) | function PreambleLayout({ provider, onSubmit, children }: PreambleLayout...
type AuthOAuthPreambleProps (line 94) | interface AuthOAuthPreambleProps {
function AuthOAuthPreamble (line 99) | function AuthOAuthPreamble({
type LinkOAuthPreambleProps (line 123) | interface LinkOAuthPreambleProps {
function LinkOAuthPreamble (line 127) | function LinkOAuthPreamble({ provider }: LinkOAuthPreambleProps) {
function ProviderIconPair (line 139) | function ProviderIconPair({ children }: { children: ReactNode }) {
FILE: applications/web/src/features/blog/components/blog-post-cta.tsx
function BlogPostCta (line 5) | function BlogPostCta() {
FILE: applications/web/src/features/dashboard/components/event-graph.tsx
constant DAYS_BEFORE (line 16) | const DAYS_BEFORE = 7;
constant DAYS_AFTER (line 17) | const DAYS_AFTER = 7;
constant TOTAL_DAYS (line 18) | const TOTAL_DAYS = DAYS_BEFORE + 1 + DAYS_AFTER;
constant GRAPH_HEIGHT (line 19) | const GRAPH_HEIGHT = 96;
constant MIN_BAR_HEIGHT (line 20) | const MIN_BAR_HEIGHT = 20;
type Period (line 34) | type Period = "past" | "today" | "future";
constant MS_PER_DAY (line 42) | const MS_PER_DAY = 86_400_000;
type DayData (line 61) | interface DayData {
constant GROWTH_SPACE (line 79) | const GROWTH_SPACE = GRAPH_HEIGHT - MIN_BAR_HEIGHT;
function resolveBarHeight (line 81) | function resolveBarHeight(count: number, maxCount: number): number {
function resolveWeekTotal (line 105) | function resolveWeekTotal(days: DayData[]): number {
function resolveEventCount (line 109) | function resolveEventCount(hoverIndex: number | null, days: DayData[]): ...
function resolveLabel (line 114) | function resolveLabel(hoverIndex: number | null, days: DayData[]): string {
function resolveDataAttr (line 119) | function resolveDataAttr(condition: boolean): "" | undefined {
type EventGraphSummaryProps (line 124) | interface EventGraphSummaryProps {
function EventGraphSummary (line 128) | function EventGraphSummary({ days }: EventGraphSummaryProps) {
function EventGraphEventCount (line 137) | function EventGraphEventCount({ days }: EventGraphSummaryProps) {
function EventGraphLabel (line 148) | function EventGraphLabel({ days }: EventGraphSummaryProps) {
constant ANIMATED_TRANSITION (line 159) | const ANIMATED_TRANSITION = { duration: 0.3, ease: [0.4, 0, 0.2, 1] as c...
constant INSTANT_TRANSITION (line 160) | const INSTANT_TRANSITION = { duration: 0 };
function resolveBarTransition (line 162) | function resolveBarTransition(shouldAnimate: boolean, dayIndex: number) {
function useIsActiveDragTarget (line 167) | function useIsActiveDragTarget(index: number): boolean {
type EventGraphBarProps (line 175) | interface EventGraphBarProps {
type EventGraphBarsProps (line 217) | interface EventGraphBarsProps {
function EventGraphBars (line 222) | function EventGraphBars({ days, shouldAnimate }: EventGraphBarsProps) {
function EventGraph (line 288) | function EventGraph() {
FILE: applications/web/src/features/dashboard/components/metadata-row.tsx
type MetadataRowProps (line 11) | interface MetadataRowProps {
function MetadataRow (line 19) | function MetadataRow({ label, value, icon, truncate = false, to }: Metad...
FILE: applications/web/src/features/dashboard/components/sync-status.tsx
function SyncProgressCircle (line 8) | function SyncProgressCircle({ percent }: { percent: number }) {
function SyncProgressIndicator (line 41) | function SyncProgressIndicator() {
function SyncStatusLabel (line 55) | function SyncStatusLabel() {
function SyncTooltipContent (line 66) | function SyncTooltipContent() {
function SyncStatus (line 74) | function SyncStatus() {
FILE: applications/web/src/features/dashboard/components/upgrade-card.tsx
function UpgradeCard (line 57) | function UpgradeCard({ children, className }: PropsWithChildren<{ classN...
function UpgradeCardSection (line 65) | function UpgradeCardSection({ children, gap }: PropsWithChildren<{ gap?:...
type UpgradeCardToggleProps (line 69) | type UpgradeCardToggleProps = PropsWithChildren<{
function UpgradeCardToggle (line 74) | function UpgradeCardToggle({ checked, onCheckedChange, children }: Upgra...
function UpgradeCardFeature (line 91) | function UpgradeCardFeature({ children }: PropsWithChildren) {
function UpgradeCardFeatureIcon (line 95) | function UpgradeCardFeatureIcon({ children }: PropsWithChildren) {
function UpgradeCardActions (line 99) | function UpgradeCardActions({ children }: PropsWithChildren) {
FILE: applications/web/src/features/marketing/components/marketing-cta.tsx
function MarketingCtaSection (line 3) | function MarketingCtaSection({ children }: PropsWithChildren) {
function MarketingCtaCard (line 11) | function MarketingCtaCard({ children }: PropsWithChildren) {
FILE: applications/web/src/features/marketing/components/marketing-faq.tsx
function MarketingFaqSection (line 4) | function MarketingFaqSection({ children }: PropsWithChildren) {
function MarketingFaqList (line 8) | function MarketingFaqList({ children }: PropsWithChildren) {
function MarketingFaqItem (line 16) | function MarketingFaqItem({ children }: PropsWithChildren) {
function MarketingFaqQuestion (line 24) | function MarketingFaqQuestion({ children }: PropsWithChildren) {
FILE: applications/web/src/features/marketing/components/marketing-feature-bento.tsx
type MarketingFeatureBentoCardProps (line 4) | type MarketingFeatureBentoCardProps = PropsWithChildren<{ className?: st...
function MarketingFeatureBentoSection (line 6) | function MarketingFeatureBentoSection({ children, id }: PropsWithChildre...
function MarketingFeatureBentoGrid (line 10) | function MarketingFeatureBentoGrid({ children }: PropsWithChildren) {
function MarketingFeatureBentoCard (line 18) | function MarketingFeatureBentoCard({
constant ILLUSTRATION_STYLE (line 29) | const ILLUSTRATION_STYLE = {
type MarketingFeatureBentoIllustrationProps (line 34) | type MarketingFeatureBentoIllustrationProps = PropsWithChildren<{
function MarketingFeatureBentoIllustration (line 38) | function MarketingFeatureBentoIllustration({ children, plain }: Marketin...
function MarketingFeatureBentoBody (line 51) | function MarketingFeatureBentoBody({ children }: PropsWithChildren) {
FILE: applications/web/src/features/marketing/components/marketing-footer.tsx
function MarketingFooter (line 4) | function MarketingFooter({ children }: PropsWithChildren) {
function MarketingFooterTagline (line 12) | function MarketingFooterTagline({ children }: PropsWithChildren) {
function MarketingFooterNav (line 20) | function MarketingFooterNav({ children }: PropsWithChildren) {
function MarketingFooterNavGroup (line 28) | function MarketingFooterNavGroup({ children }: PropsWithChildren) {
function MarketingFooterNavGroupLabel (line 36) | function MarketingFooterNavGroupLabel({ children }: PropsWithChildren) {
type MarketingFooterNavItemProps (line 44) | type MarketingFooterNavItemProps = PropsWithChildren<{
function MarketingFooterNavItem (line 49) | function MarketingFooterNavItem({ children, to, href }: MarketingFooterN...
FILE: applications/web/src/features/marketing/components/marketing-header.tsx
function MarketingHeader (line 6) | function MarketingHeader({ children }: PropsWithChildren) {
function MarketingHeaderBranding (line 19) | function MarketingHeaderBranding({ children, label }: PropsWithChildren<...
function MarketingHeaderActions (line 23) | function MarketingHeaderActions({ children }: PropsWithChildren) {
FILE: applications/web/src/features/marketing/components/marketing-how-it-works.tsx
function MarketingHowItWorksSection (line 5) | function MarketingHowItWorksSection({ children }: PropsWithChildren) {
constant ILLUSTRATION_STYLE (line 9) | const ILLUSTRATION_STYLE = {
function MarketingHowItWorksCard (line 14) | function MarketingHowItWorksCard({ children }: PropsWithChildren) {
function MarketingHowItWorksRow (line 22) | function MarketingHowItWorksRow({ children, className, reverse }: PropsW...
function MarketingHowItWorksStepBody (line 30) | function MarketingHowItWorksStepBody({
function MarketingHowItWorksStepIllustration (line 42) | function MarketingHowItWorksStepIllustration({ children, align }: PropsW...
FILE: applications/web/src/features/marketing/components/marketing-illustration-calendar.tsx
type Skew (line 8) | interface Skew {
type SkewTuple (line 14) | type SkewTuple = [Skew, Skew, Skew];
constant CALENDAR_COLUMNS (line 16) | const CALENDAR_COLUMNS = 7;
constant CALENDAR_ROWS (line 17) | const CALENDAR_ROWS = 6;
constant CALENDAR_DAYS_IN_MONTH (line 18) | const CALENDAR_DAYS_IN_MONTH = 31;
constant CALENDAR_CELLS (line 19) | const CALENDAR_CELLS = CALENDAR_COLUMNS * CALENDAR_ROWS;
constant CALENDAR_ANIMATION_EASE (line 20) | const CALENDAR_ANIMATION_EASE = [0.16, 0.85, 0.2, 1] as const;
constant CALENDAR_DAY_NUMBERS (line 22) | const CALENDAR_DAY_NUMBERS = Array.from(
type MarketingIllustrationCalendarCardProps (line 27) | interface MarketingIllustrationCalendarCardProps {
type CalendarDayProps (line 47) | interface CalendarDayProps {
function MarketingIllustrationCalendarCard (line 72) | function MarketingIllustrationCalendarCard({
function MarketingIllustrationCalendar (line 94) | function MarketingIllustrationCalendar({ children }: PropsWithChildren) {
FILE: applications/web/src/features/marketing/components/marketing-pricing-section.tsx
type ClassNameProps (line 10) | type ClassNameProps = PropsWithChildren<{ className?: string }>;
type MarketingPricingCardProps (line 11) | type MarketingPricingCardProps = ClassNameProps & VariantProps<typeof ma...
type MarketingPricingFeatureValueKind (line 12) | type MarketingPricingFeatureValueKind =
type MarketingPricingPlanCardProps (line 18) | type MarketingPricingPlanCardProps = {
function MarketingPricingSection (line 40) | function MarketingPricingSection({ children, id }: PropsWithChildren<{ i...
function MarketingPricingIntro (line 44) | function MarketingPricingIntro({ children }: PropsWithChildren) {
function MarketingPricingComparisonGrid (line 48) | function MarketingPricingComparisonGrid({ children }: PropsWithChildren) {
function MarketingPricingComparisonSpacer (line 54) | function MarketingPricingComparisonSpacer() {
function MarketingPricingCard (line 58) | function MarketingPricingCard({
function MarketingPricingCardBody (line 72) | function MarketingPricingCardBody({ children }: PropsWithChildren) {
function MarketingPricingCardCopy (line 76) | function MarketingPricingCardCopy({ children }: PropsWithChildren) {
function MarketingPricingCardAction (line 80) | function MarketingPricingCardAction({ children }: PropsWithChildren) {
function resolveCopyTone (line 121) | function resolveCopyTone(tone: "default" | "inverse"): "inverseMuted" | ...
function MarketingPricingPlanCard (line 126) | function MarketingPricingPlanCard({
function MarketingPricingFeatureMatrix (line 161) | function MarketingPricingFeatureMatrix({ children }: PropsWithChildren) {
function MarketingPricingFeatureRow (line 165) | function MarketingPricingFeatureRow({ children }: PropsWithChildren) {
function MarketingPricingFeatureLabel (line 173) | function MarketingPricingFeatureLabel({ children }: PropsWithChildren) {
function MarketingPricingFeatureValue (line 177) | function MarketingPricingFeatureValue({ children }: PropsWithChildren) {
function MarketingPricingFeatureDisplay (line 181) | function MarketingPricingFeatureDisplay({
FILE: applications/web/src/generated/tanstack/route-tree.generated.ts
type FileRoutesByFullPath (line 324) | interface FileRoutesByFullPath {
type FileRoutesByTo (line 369) | interface FileRoutesByTo {
type FileRoutesById (line 410) | interface FileRoutesById {
type FileRouteTypes (line 461) | interface FileRouteTypes {
type RootRouteChildren (line 600) | interface RootRouteChildren {
type FileRoutesByPath (line 608) | interface FileRoutesByPath {
type authRouteRouteChildren (line 948) | interface authRouteRouteChildren {
type dashboardDashboardAccountsRouteRouteChildren (line 970) | interface dashboardDashboardAccountsRouteRouteChildren {
type dashboardDashboardConnectRouteRouteChildren (line 991) | interface dashboardDashboardConnectRouteRouteChildren {
type dashboardDashboardSettingsRouteRouteChildren (line 1005) | interface dashboardDashboardSettingsRouteRouteChildren {
type dashboardRouteRouteChildren (line 1028) | interface dashboardRouteRouteChildren {
type marketingBlogRouteRouteChildren (line 1062) | interface marketingBlogRouteRouteChildren {
type marketingRouteRouteChildren (line 1075) | interface marketingRouteRouteChildren {
type oauthAuthRouteRouteChildren (line 1093) | interface oauthAuthRouteRouteChildren {
type oauthDashboardConnectRouteRouteChildren (line 1107) | interface oauthDashboardConnectRouteRouteChildren {
type oauthDashboardRouteRouteChildren (line 1135) | interface oauthDashboardRouteRouteChildren {
type oauthRouteRouteChildren (line 1146) | interface oauthRouteRouteChildren {
FILE: applications/web/src/hooks/use-animated-swr.ts
type AnimatedSWRResponse (line 5) | interface AnimatedSWRResponse<T> extends SWRResponse<T> {
function useAnimatedSWR (line 9) | function useAnimatedSWR<T>(key: string, config?: SWRConfiguration<T>): A...
FILE: applications/web/src/hooks/use-api-tokens.ts
type ApiToken (line 4) | interface ApiToken {
type CreatedApiToken (line 13) | interface CreatedApiToken {
FILE: applications/web/src/hooks/use-entitlements.ts
type EntitlementLimit (line 6) | interface EntitlementLimit {
type Entitlements (line 11) | interface Entitlements {
constant USAGE_CACHE_KEY (line 19) | const USAGE_CACHE_KEY = "/api/entitlements";
function useEntitlements (line 21) | function useEntitlements() {
function useMutateEntitlements (line 51) | function useMutateEntitlements() {
function canAddMore (line 83) | function canAddMore(entitlement: EntitlementLimit | undefined): boolean {
FILE: applications/web/src/hooks/use-events.ts
type CalendarEvent (line 6) | interface CalendarEvent {
constant DAYS_PER_PAGE (line 16) | const DAYS_PER_PAGE = 7;
function useEvents (line 38) | function useEvents() {
function resolveEvents (line 72) | function resolveEvents(data: CalendarEvent[][] | undefined): CalendarEve...
FILE: applications/web/src/hooks/use-passkeys.ts
type Passkey (line 4) | interface Passkey {
FILE: applications/web/src/hooks/use-session.ts
type SessionUser (line 4) | interface SessionUser {
function useSession (line 22) | function useSession() {
FILE: applications/web/src/hooks/use-start-of-today.ts
function resolveStartOfToday (line 3) | function resolveStartOfToday(): Date {
function resolveMillisecondsUntilTomorrow (line 9) | function resolveMillisecondsUntilTomorrow(): number {
function useStartOfToday (line 16) | function useStartOfToday(): Date {
FILE: applications/web/src/hooks/use-subscription.ts
type SubscriptionState (line 5) | interface SubscriptionState {
type ActiveSubscription (line 10) | interface ActiveSubscription {
type CustomerStateResponse (line 14) | interface CustomerStateResponse {
constant SUBSCRIPTION_STATE_CACHE_KEY (line 18) | const SUBSCRIPTION_STATE_CACHE_KEY = "customer-state";
type UseSubscriptionOptions (line 40) | interface UseSubscriptionOptions {
function useSubscription (line 53) | function useSubscription(options: UseSubscriptionOptions = {}) {
function fetchSubscriptionStateWithApi (line 64) | async function fetchSubscriptionStateWithApi(
FILE: applications/web/src/illustrations/how-it-works-configure.tsx
function MiniToggle (line 1) | function MiniToggle({ checked }: { checked: boolean }) {
constant SETTINGS (line 17) | const SETTINGS = [
function MiniSettingsRow (line 25) | function MiniSettingsRow({ label, checked }: { label: string; checked: b...
function HowItWorksConfigure (line 36) | function HowItWorksConfigure() {
FILE: applications/web/src/illustrations/how-it-works-connect.tsx
constant PROVIDERS (line 3) | const PROVIDERS = [
function MiniMenuRow (line 11) | function MiniMenuRow({ icon, label }: { icon: string; label: string }) {
function HowItWorksConnect (line 21) | function HowItWorksConnect() {
FILE: applications/web/src/illustrations/how-it-works-sync.tsx
constant PLACEHOLDER_COUNTS (line 8) | const PLACEHOLDER_COUNTS = [3, 5, 2, 7, 4, 6, 1, 8, 3, 5, 4, 6, 2, 7, 5];
constant GRAPH_HEIGHT (line 9) | const GRAPH_HEIGHT = 128;
constant MIN_BAR_HEIGHT (line 10) | const MIN_BAR_HEIGHT = 16;
constant GROWTH_SPACE (line 11) | const GROWTH_SPACE = GRAPH_HEIGHT - MIN_BAR_HEIGHT;
constant MAX_COUNT (line 12) | const MAX_COUNT = Math.max(...PLACEHOLDER_COUNTS);
constant TOTAL_EVENTS (line 13) | const TOTAL_EVENTS = PLACEHOLDER_COUNTS.reduce((sum, count) => sum + cou...
constant TODAY_INDEX (line 14) | const TODAY_INDEX = 7;
type Period (line 16) | type Period = "past" | "today" | "future";
function resolvePeriod (line 30) | function resolvePeriod(index: number): Period {
function resolveBarHeight (line 36) | function resolveBarHeight(count: number): number {
type BarData (line 40) | interface BarData {
constant BARS (line 46) | const BARS: BarData[] = PLACEHOLDER_COUNTS.map((count, index) => ({
constant BAR_TRANSITION_EASE (line 52) | const BAR_TRANSITION_EASE = [0.4, 0, 0.2, 1] as const;
function HowItWorksSync (line 54) | function HowItWorksSync() {
FILE: applications/web/src/illustrations/marketing-illustration-contributors.tsx
constant VISIBLE_COUNT (line 8) | const VISIBLE_COUNT = 3;
constant ROTATE_INTERVAL_MS (line 9) | const ROTATE_INTERVAL_MS = 1800;
constant FALLBACK_ROW_HEIGHT (line 10) | const FALLBACK_ROW_HEIGHT = 36;
constant SLOT_STYLES (line 12) | const SLOT_STYLES = [
constant TRANSITION (line 18) | const TRANSITION = {
type Contributor (line 24) | type Contributor = (typeof CONTRIBUTORS)[number];
function MarketingIllustrationContributors (line 52) | function MarketingIllustrationContributors() {
FILE: applications/web/src/illustrations/marketing-illustration-providers.tsx
constant ORBIT_ITEMS (line 3) | const ORBIT_ITEMS = Object.entries(providerIcons).reverse();
constant ORBIT_DURATION (line 4) | const ORBIT_DURATION = 12;
constant RADIUS (line 5) | const RADIUS = 110;
constant ICON_SIZE (line 6) | const ICON_SIZE = 32;
function MarketingIllustrationProviders (line 8) | function MarketingIllustrationProviders() {
FILE: applications/web/src/illustrations/marketing-illustration-setup.tsx
type Phase (line 6) | type Phase = "button" | "cursor" | "click" | "syncing" | "done";
constant PHASE_DURATIONS (line 8) | const PHASE_DURATIONS: Record<Phase, number> = {
constant PHASE_ORDER (line 16) | const PHASE_ORDER: Phase[] = ["button", "cursor", "click", "syncing", "d...
constant TRANSITION_ENTER (line 18) | const TRANSITION_ENTER = {
constant INITIAL (line 24) | const INITIAL = { opacity: 0, scale: 0.9, filter: "blur(4px)" };
constant ANIMATE (line 25) | const ANIMATE = { opacity: 1, scale: 1, filter: "blur(0px)" };
constant EXIT (line 26) | const EXIT = { opacity: 0, scale: 0.9, filter: "blur(4px)" };
constant CIRCLE_RADIUS (line 28) | const CIRCLE_RADIUS = 16;
constant CIRCLE_CIRCUMFERENCE (line 29) | const CIRCLE_CIRCUMFERENCE = 2 * Math.PI * CIRCLE_RADIUS;
function SyncButton (line 31) | function SyncButton({ pressed }: { pressed: boolean }) {
function SyncCircle (line 45) | function SyncCircle() {
function MarketingIllustrationSetup (line 86) | function MarketingIllustrationSetup() {
FILE: applications/web/src/illustrations/marketing-illustration-sync.tsx
constant PATH_UPPER (line 2) | const PATH_UPPER = `M -10,20 L ${210 - R},20 Q 210,20 210,${20 + R} L 21...
constant PATH_LOWER (line 3) | const PATH_LOWER = `M -10,80 L ${210 - R},80 Q 210,80 210,${80 - R} L 21...
constant ICON_SIZE (line 5) | const ICON_SIZE = 24;
constant PROVIDERS (line 7) | const PROVIDERS = [
function MarketingIllustrationSync (line 13) | function MarketingIllustrationSync() {
FILE: applications/web/src/lib/analytics.ts
constant CONSENT_COOKIE (line 3) | const CONSENT_COOKIE = "keeper.analytics_consent";
constant CONSENT_MAX_AGE (line 4) | const CONSENT_MAX_AGE = 60 * 60 * 24 * 182;
function resolveConsentState (line 6) | function resolveConsentState(granted: boolean): "granted" | "denied" {
function readCookieSource (line 11) | function readCookieSource(cookieHeader?: string): string {
function readConsentValue (line 17) | function readConsentValue(cookieHeader?: string): string | null {
function resolveEffectiveConsent (line 47) | function resolveEffectiveConsent(gdprApplies: boolean, cookieHeader?: st...
constant ANALYTICS_EVENTS (line 61) | const ANALYTICS_EVENTS = {
type EventProperties (line 95) | type EventProperties = Record<string, string | number | boolean>;
type IdentifyProps (line 101) | interface IdentifyProps {
type ConversionOptions (line 115) | interface ConversionOptions {
FILE: applications/web/src/lib/auth-capabilities.ts
type SocialProviderId (line 5) | type SocialProviderId = keyof AuthCapabilities["socialProviders"];
type CredentialField (line 7) | interface CredentialField {
FILE: applications/web/src/lib/auth.ts
function authPost (line 4) | async function authPost(url: string, body: Record<string, unknown> = {})...
function authJsonPost (line 17) | async function authJsonPost(url: string, body: Record<string, unknown>):...
FILE: applications/web/src/lib/blog-posts.ts
type BlogPostMetadata (line 4) | interface BlogPostMetadata {
type BlogPost (line 14) | interface BlogPost {
function findBlogPostBySlug (line 22) | function findBlogPostBySlug(slug: string): BlogPost | undefined {
function formatIsoDate (line 41) | function formatIsoDate(isoDate: string): string {
FILE: applications/web/src/lib/fetcher.ts
class HttpError (line 1) | class HttpError extends Error {
method constructor (line 2) | constructor(
function fetcher (line 11) | async function fetcher<T>(url: string): Promise<T> {
function apiFetch (line 17) | async function apiFetch(
FILE: applications/web/src/lib/mcp-auth-flow.ts
type SearchParams (line 3) | type SearchParams = Record<string, unknown>;
type StringSearchParams (line 4) | type StringSearchParams = Record<string, string>;
constant DEFAULT_POST_AUTH_PATH (line 6) | const DEFAULT_POST_AUTH_PATH = "/dashboard";
FILE: applications/web/src/lib/page-metadata.ts
function formatMonthYear (line 7) | function formatMonthYear(isoDate: string): string {
type PageMetadata (line 11) | interface PageMetadata {
FILE: applications/web/src/lib/pluralize.ts
function pluralize (line 1) | function pluralize(count: number, singular: string, plural: string = `${...
FILE: applications/web/src/lib/route-access-guards.ts
type RedirectTarget (line 1) | type RedirectTarget = "/dashboard" | "/login";
type SubscriptionPlan (line 3) | type SubscriptionPlan = "free" | "pro";
FILE: applications/web/src/lib/router-context.ts
type AppAuthContext (line 3) | interface AppAuthContext {
type AppJsonFetcher (line 7) | type AppJsonFetcher = <T>(path: string, init?: RequestInit) => Promise<T>;
type ViteScript (line 9) | interface ViteScript {
type ViteAssets (line 14) | interface ViteAssets {
type AppRouterContext (line 22) | interface AppRouterContext {
FILE: applications/web/src/lib/runtime-config.ts
type PublicRuntimeConfig (line 3) | interface PublicRuntimeConfig {
type RuntimeConfigSource (line 13) | interface RuntimeConfigSource {
type ServerRuntimeConfigOptions (line 31) | interface ServerRuntimeConfigOptions {
type Window (line 83) | interface Window {
FILE: applications/web/src/lib/seo.ts
constant SITE_URL (line 1) | const SITE_URL = "https://keeper.sh";
constant SITE_NAME (line 2) | const SITE_NAME = "Keeper.sh";
function canonicalUrl (line 4) | function canonicalUrl(path: string): string {
function jsonLdScript (line 8) | function jsonLdScript(data: Record<string, unknown>) {
function seoMeta (line 12) | function seoMeta({
function breadcrumbSchema (line 72) | function breadcrumbSchema(
function webPageSchema (line 87) | function webPageSchema(name: string, description: string, path: string) {
function softwareApplicationSchema (line 100) | function softwareApplicationSchema() {
function collectionPageSchema (line 140) | function collectionPageSchema(posts: Array<{ slug: string; metadata: { t...
function blogPostingSchema (line 168) | function blogPostingSchema(post: {
FILE: applications/web/src/lib/serialized-mutate.ts
type ErrorHandler (line 1) | type ErrorHandler = (error: unknown) => void;
type QueuedPatch (line 3) | interface QueuedPatch {
function runFlush (line 12) | function runFlush(
function drainPatchQueue (line 22) | function drainPatchQueue(key: string) {
function serializedPatch (line 38) | function serializedPatch(
type QueuedCall (line 71) | interface QueuedCall {
function drainCallQueue (line 79) | function drainCallQueue(key: string) {
function serializedCall (line 95) | function serializedCall(
function defaultOnError (line 118) | function defaultOnError() {}
FILE: applications/web/src/lib/session-cookie.ts
constant SESSION_COOKIE (line 1) | const SESSION_COOKIE = "keeper.has_session=1";
function hasSessionCookie (line 3) | function hasSessionCookie(cookieHeader?: string): boolean {
FILE: applications/web/src/lib/swr.ts
function invalidateAccountsAndSources (line 7) | function invalidateAccountsAndSources(
FILE: applications/web/src/lib/time.ts
constant MINS_PER_HOUR (line 1) | const MINS_PER_HOUR = 60;
constant HOURS_PER_DAY (line 2) | const HOURS_PER_DAY = 24;
constant MINS_PER_DAY (line 3) | const MINS_PER_DAY = MINS_PER_HOUR * HOURS_PER_DAY;
FILE: applications/web/src/providers/sync-provider-logic.ts
type IncomingSocketAction (line 4) | type IncomingSocketAction =
type AggregateDecision (line 10) | interface AggregateDecision {
FILE: applications/web/src/providers/sync-provider.tsx
type ConnectionState (line 9) | interface ConnectionState {
constant MAX_RECONNECT_DELAY_MS (line 21) | const MAX_RECONNECT_DELAY_MS = 30_000;
constant BASE_RECONNECT_DELAY_MS (line 22) | const BASE_RECONNECT_DELAY_MS = 2_000;
constant INITIAL_AGGREGATE_TIMEOUT_MS (line 23) | const INITIAL_AGGREGATE_TIMEOUT_MS = 10_000;
constant INITIAL_SYNC_STATE (line 25) | const INITIAL_SYNC_STATE: CompositeSyncState = {
function SyncProvider (line 229) | function SyncProvider() {
FILE: applications/web/src/routeTree.gen.ts
type FileRoutesByFullPath (line 324) | interface FileRoutesByFullPath {
type FileRoutesByTo (line 369) | interface FileRoutesByTo {
type FileRoutesById (line 410) | interface FileRoutesById {
type FileRouteTypes (line 461) | interface FileRouteTypes {
type RootRouteChildren (line 600) | interface RootRouteChildren {
type FileRoutesByPath (line 608) | interface FileRoutesByPath {
type authRouteRouteChildren (line 948) | interface authRouteRouteChildren {
type dashboardDashboardAccountsRouteRouteChildren (line 970) | interface dashboardDashboardAccountsRouteRouteChildren {
type dashboardDashboardConnectRouteRouteChildren (line 991) | interface dashboardDashboardConnectRouteRouteChildren {
type dashboardDashboardSettingsRouteRouteChildren (line 1005) | interface dashboardDashboardSettingsRouteRouteChildren {
type dashboardRouteRouteChildren (line 1028) | interface dashboardRouteRouteChildren {
type marketingBlogRouteRouteChildren (line 1062) | interface marketingBlogRouteRouteChildren {
type marketingRouteRouteChildren (line 1075) | interface marketingRouteRouteChildren {
type oauthAuthRouteRouteChildren (line 1093) | interface oauthAuthRouteRouteChildren {
type oauthDashboardConnectRouteRouteChildren (line 1107) | interface oauthDashboardConnectRouteRouteChildren {
type oauthDashboardRouteRouteChildren (line 1135) | interface oauthDashboardRouteRouteChildren {
type oauthRouteRouteChildren (line 1146) | interface oauthRouteRouteChildren {
FILE: applications/web/src/router.ts
type CreateAppRouterOptions (line 11) | interface CreateAppRouterOptions {
function getConfiguredApiOrigin (line 16) | function getConfiguredApiOrigin(): string | undefined {
function resolveApiOrigin (line 24) | function resolveApiOrigin(request: Request | undefined): string {
function resolveWebOrigin (line 41) | function resolveWebOrigin(request: Request | undefined): string {
function createJsonFetcher (line 53) | function createJsonFetcher(
function createApiFetcher (line 78) | function createApiFetcher(
function createWebFetcher (line 86) | function createWebFetcher(
function resolveRuntimeConfig (line 94) | function resolveRuntimeConfig(request: Request | undefined): PublicRunti...
function createSessionChecker (line 105) | function createSessionChecker(
function buildRouterContext (line 117) | function buildRouterContext(
function createAppRouter (line 132) | function createAppRouter(options: CreateAppRouterOptions = {}) {
type Register (line 144) | interface Register {
FILE: applications/web/src/routes/(auth)/forgot-password.tsx
function ForgotPasswordPage (line 26) | function ForgotPasswordPage() {
function SuccessState (line 73) | function SuccessState() {
FILE: applications/web/src/routes/(auth)/login.tsx
function LoginPage (line 26) | function LoginPage() {
FILE: applications/web/src/routes/(auth)/register.tsx
function RegisterPage (line 26) | function RegisterPage() {
FILE: applications/web/src/routes/(auth)/reset-password.tsx
type SearchParams (line 15) | type SearchParams = { token?: string };
function ResetPasswordPage (line 31) | function ResetPasswordPage() {
function ResetPasswordForm (line 38) | function ResetPasswordForm({ token }: { token: string }) {
function SuccessState (line 105) | function SuccessState() {
function InvalidTokenState (line 126) | function InvalidTokenState() {
FILE: applications/web/src/routes/(auth)/route.tsx
function AuthLayout (line 22) | function AuthLayout() {
FILE: applications/web/src/routes/(auth)/verify-authentication.tsx
function VerifyAuthenticationPage (line 10) | function VerifyAuthenticationPage() {
FILE: applications/web/src/routes/(auth)/verify-email.tsx
function VerifyEmailPage (line 21) | function VerifyEmailPage() {
FILE: applications/web/src/routes/(dashboard)/dashboard/accounts/$accountId.$calendarId.tsx
type SyncSetting (line 66) | interface SyncSetting {
constant SYNC_SETTINGS (line 72) | const SYNC_SETTINGS: SyncSetting[] = [
constant EXCLUSION_SETTINGS (line 77) | const EXCLUSION_SETTINGS: SyncSetting[] = [
constant PROVIDER_EXCLUSION_SETTINGS (line 81) | const PROVIDER_EXCLUSION_SETTINGS: SyncSetting[] = [
constant PROVIDERS_WITH_EXTRA_SETTINGS (line 86) | const PROVIDERS_WITH_EXTRA_SETTINGS = new Set(["google"]);
function patchSource (line 88) | function patchSource(
function useSeedCalendarDetail (line 112) | function useSeedCalendarDetail(calendarId: string, calendar: CalendarDet...
function CalendarDetailPage (line 125) | function CalendarDetailPage() {
function CalendarPrevNext (line 167) | function CalendarPrevNext({ calendarId }: { calendarId: string }) {
function CalendarHeader (line 190) | function CalendarHeader({ account }: { account: CalendarAccount }) {
function CalendarTitle (line 205) | function CalendarTitle() {
function RenameSection (line 210) | function RenameSection({ calendarId }: { calendarId: string }) {
function RenameItem (line 224) | function RenameItem({ calendarId }: { calendarId: string }) {
function RenameItemValue (line 242) | function RenameItemValue() {
function DestinationsSeed (line 258) | function DestinationsSeed({ calendarId }: { calendarId: string }) {
function DestinationsSection (line 271) | function DestinationsSection({ calendarId }: { calendarId: string }) {
function DestinationCheckboxItem (line 305) | function DestinationCheckboxItem({
function DestinationCheckboxIndicator (line 394) | function DestinationCheckboxIndicator({ destinationId }: { destinationId...
function SyncSettingsSection (line 406) | function SyncSettingsSection({ calendarId }: { calendarId: string }) {
function SyncEventNameDisabledProvider (line 436) | function SyncEventNameDisabledProvider({ locked, children }: { locked: b...
function SyncEventNameTemplateItem (line 441) | function SyncEventNameTemplateItem({ calendarId, locked }: { calendarId:...
function SyncEventNameTemplateInput (line 465) | function SyncEventNameTemplateInput({ template }: { template: string }) {
constant TEMPLATE_VARIABLES (line 469) | const TEMPLATE_VARIABLES = { calendar_name: "Calendar Name", event_name:...
function SyncEventNameTemplateValue (line 471) | function SyncEventNameTemplateValue() {
function SyncEventNameToggle (line 492) | function SyncEventNameToggle({ calendarId, locked }: { calendarId: strin...
function SyncEventNameToggleIndicator (line 528) | function SyncEventNameToggleIndicator({ disabled }: { disabled: boolean ...
function ExclusionsSection (line 540) | function ExclusionsSection({ calendarId, provider }: { calendarId: strin...
function ExcludeFieldToggle (line 572) | function ExcludeFieldToggle({
function ExcludeFieldToggleIndicator (line 617) | function ExcludeFieldToggleIndicator({ field, matchesField, disabled }: ...
function CalendarInfoSection (line 630) | function CalendarInfoSection({ account, accountId }: { account: Calendar...
FILE: applications/web/src/routes/(dashboard)/dashboard/accounts/$accountId.index.tsx
function CalendarList (line 35) | function CalendarList({ calendars, accountId }: { calendars: CalendarSou...
function AccountDetailPage (line 56) | function AccountDetailPage() {
function AccountPrevNext (line 143) | function AccountPrevNext({ accountId }: { accountId: string }) {
FILE: applications/web/src/routes/(dashboard)/dashboard/accounts/$accountId.setup.tsx
constant VALID_STEPS (line 25) | const VALID_STEPS = ["select", "rename", "destinations", "sources"] as c...
type SetupStep (line 26) | type SetupStep = (typeof VALID_STEPS)[number];
type SetupSearch (line 28) | interface SetupSearch {
type MappingRoute (line 34) | type MappingRoute = "destinations" | "sources";
type MappingResponseKey (line 35) | type MappingResponseKey = "destinationIds" | "sourceIds";
type CalendarMappingData (line 36) | type CalendarMappingData = Partial<Record<MappingResponseKey, string[]>>;
function isValidStep (line 38) | function isValidStep(value: unknown): value is SetupStep {
function parseSearchIndex (line 43) | function parseSearchIndex(value: unknown): number | undefined {
function parseSelectedIds (line 62) | function parseSelectedIds(commaIds: string | undefined): Set<string> {
function resolveStepCalendarIndex (line 67) | function resolveStepCalendarIndex(index: number, count: number): number {
type SetupWorkflowData (line 73) | interface SetupWorkflowData {
function resolveSetupWorkflowData (line 82) | function resolveSetupWorkflowData({
function resolveNextIndex (line 108) | function resolveNextIndex(currentIndex: number, totalCount: number): num...
type SetupStepActions (line 114) | interface SetupStepActions {
function createSetupStepActions (line 120) | function createSetupStepActions({
function SetupStepContent (line 170) | function SetupStepContent({
function buildMappingData (line 217) | function buildMappingData(responseKey: MappingResponseKey, ids: string[]...
function useCalendarMapping (line 221) | function useCalendarMapping({
function AccountSetupPage (line 271) | function AccountSetupPage() {
function SelectSection (line 324) | function SelectSection({
function RenameSection (line 399) | function RenameSection({
function EmptyStepSection (line 457) | function EmptyStepSection({ heading, message, buttonLabel, onNext }: {
function DestinationsSection (line 473) | function DestinationsSection({
function SourcesSection (line 537) | function SourcesSection({
FILE: applications/web/src/routes/(dashboard)/dashboard/accounts/route.tsx
function AccountsLayout (line 7) | function AccountsLayout() {
FILE: applications/web/src/routes/(dashboard)/dashboard/connect/index.tsx
function ConnectPage (line 20) | function ConnectPage() {
FILE: applications/web/src/routes/(dashboard)/dashboard/connect/route.tsx
function ConnectLayout (line 7) | function ConnectLayout() {
FILE: applications/web/src/routes/(dashboard)/dashboard/events/index.tsx
type DayGroup (line 16) | interface DayGroup {
type DaySectionProps (line 21) | interface DaySectionProps {
type EventRowProps (line 26) | interface EventRowProps {
type LoadMoreSentinelProps (line 30) | interface LoadMoreSentinelProps {
function EventsPage (line 51) | function EventsPage() {
function EventsContent (line 60) | function EventsContent() {
function LoadMoreSentinel (line 89) | function LoadMoreSentinel({ isValidating, hasMore, onLoadMore }: LoadMor...
function LoadingIndicator (line 116) | function LoadingIndicator() {
function resolveEventRowClassName (line 143) | function resolveEventRowClassName(past: boolean): string {
FILE: applications/web/src/routes/(dashboard)/dashboard/feedback.tsx
function FeedbackPage (line 15) | function FeedbackPage() {
FILE: applications/web/src/routes/(dashboard)/dashboard/ical.tsx
type ICalTokenResponse (line 48) | type ICalTokenResponse = {
type FeedSettings (line 53) | interface FeedSettings {
function patchFeedSettings (line 65) | function patchFeedSettings(
function ICalPage (line 88) | function ICalPage() {
function ICalLinkSection (line 107) | function ICalLinkSection() {
function CopyButton (line 131) | function CopyButton({ value }: { value: string | null }) {
function CopyIcon (line 156) | function CopyIcon() {
function FeedSettingsSeed (line 161) | function FeedSettingsSeed() {
function FeedSettingsToggles (line 173) | function FeedSettingsToggles({ locked }: { locked: boolean }) {
constant TEMPLATE_VARIABLES (line 193) | const TEMPLATE_VARIABLES = { event_name: "Event Name", calendar_name: "C...
function EventNameDisabledProvider (line 195) | function EventNameDisabledProvider({ locked, children }: { locked: boole...
function EventNameTemplateItem (line 200) | function EventNameTemplateItem({ locked }: { locked: boolean }) {
function EventNameTemplateValue (line 224) | function EventNameTemplateValue() {
function EventNameToggle (line 246) | function EventNameToggle({ locked }: { locked: boolean }) {
function FeedSettingToggle (line 279) | function FeedSettingToggle({
function SourceSelectionSection (line 312) | function SourceSelectionSection() {
function SourceCheckboxItem (line 359) | function SourceCheckboxItem({
FILE: applications/web/src/routes/(dashboard)/dashboard/index.tsx
function DashboardPage (line 41) | function DashboardPage() {
function CalendarsMenu (line 107) | function CalendarsMenu() {
function AccountsPopover (line 181) | function AccountsPopover() {
FILE: applications/web/src/routes/(dashboard)/dashboard/integrations/index.tsx
type SearchParams (line 6) | interface SearchParams {
function parseSearchError (line 10) | function parseSearchError(value: unknown): string | undefined {
function OAuthCallbackErrorPage (line 27) | function OAuthCallbackErrorPage() {
FILE: applications/web/src/routes/(dashboard)/dashboard/report.tsx
function ReportPage (line 16) | function ReportPage() {
FILE: applications/web/src/routes/(dashboard)/dashboard/settings/api-tokens.tsx
function CopyTokenIcon (line 44) | function CopyTokenIcon({ copied }: { copied: boolean }) {
function resolveApiLimitLabel (line 51) | function resolveApiLimitLabel(plan: string | null): string {
function ApiTokensPage (line 58) | function ApiTokensPage() {
function CreateSubmitButton (line 207) | function CreateSubmitButton({ isCreating }: { isCreating: boolean }) {
function CreateTokenButton (line 223) | function CreateTokenButton({
FILE: applications/web/src/routes/(dashboard)/dashboard/settings/change-password.tsx
function resolveInputTone (line 27) | function resolveInputTone(error: string | null): "error" | "neutral" {
function ChangePasswordPage (line 32) | function ChangePasswordPage() {
FILE: applications/web/src/routes/(dashboard)/dashboard/settings/index.tsx
function loadSubscription (line 45) | async function loadSubscription(context: { runtimeConfig: { commercialMo...
function SettingsPage (line 65) | function SettingsPage() {
FILE: applications/web/src/routes/(dashboard)/dashboard/settings/passkeys.tsx
function PasskeysPage (line 43) | function PasskeysPage() {
function AddPasskeyButton (line 110) | function AddPasskeyButton({
FILE: applications/web/src/routes/(dashboard)/dashboard/settings/route.tsx
function SettingsLayout (line 7) | function SettingsLayout() {
FILE: applications/web/src/routes/(dashboard)/dashboard/upgrade/index.tsx
function resolveProPlan (line 65) | function resolveProPlan(runtimeConfig: PublicRuntimeConfig): PlanConfig {
function UpgradePage (line 73) | function UpgradePage() {
type UpgradeActionProps (line 163) | type UpgradeActionProps = {
function UpgradeAction (line 170) | function UpgradeAction({ mode, isLoading, onUpgrade, onManage }: Upgrade...
FILE: applications/web/src/routes/(dashboard)/route.tsx
function DashboardLayout (line 32) | function DashboardLayout() {
FILE: applications/web/src/routes/(marketing)/blog/$slug.tsx
function BlogPostPage (line 55) | function BlogPostPage() {
FILE: applications/web/src/routes/(marketing)/blog/index.tsx
constant BLOG_ILLUSTRATION_STYLE (line 7) | const BLOG_ILLUSTRATION_STYLE = {
function BlogDirectoryPage (line 31) | function BlogDirectoryPage() {
FILE: applications/web/src/routes/(marketing)/blog/route.tsx
function BlogRouteLayout (line 7) | function BlogRouteLayout() {
FILE: applications/web/src/routes/(marketing)/index.tsx
constant SKEW_BACK_LEFT (line 52) | const SKEW_BACK_LEFT: SkewTuple = [
constant SKEW_BACK_RIGHT (line 58) | const SKEW_BACK_RIGHT: SkewTuple = [
constant SKEW_FRONT (line 64) | const SKEW_FRONT: SkewTuple = [
type MarketingFeature (line 70) | type MarketingFeature = {
constant MARKETING_FEATURES (line 78) | const MARKETING_FEATURES: MarketingFeature[] = [
type PricingFeature (line 113) | type PricingFeature = {
type PricingPlan (line 119) | type PricingPlan = {
constant PRICING_PLANS (line 129) | const PRICING_PLANS: PricingPlan[] = [
constant PRICING_FEATURES (line 151) | const PRICING_FEATURES: PricingFeature[] = [
type HowItWorksStep (line 162) | type HowItWorksStep = {
constant HOW_IT_WORKS_STEPS (line 167) | const HOW_IT_WORKS_STEPS: HowItWorksStep[] = [
type FaqItem (line 185) | type FaqItem = {
constant FAQ_ITEMS (line 191) | const FAQ_ITEMS: FaqItem[] = [
function MarketingPage (line 245) | function MarketingPage() {
FILE: applications/web/src/routes/(marketing)/privacy.tsx
function PrivacyPage (line 25) | function PrivacyPage() {
function Section (line 186) | function Section({ title, children }: PropsWithChildren<{ title: string ...
FILE: applications/web/src/routes/(marketing)/route.tsx
type GithubStarsLoaderData (line 14) | interface GithubStarsLoaderData {
function MarketingLayout (line 42) | function MarketingLayout() {
FILE: applications/web/src/routes/(marketing)/terms.tsx
function TermsPage (line 25) | function TermsPage() {
function Section (line 222) | function Section({ title, children }: PropsWithChildren<{ title: string ...
FILE: applications/web/src/routes/(oauth)/auth/google.tsx
function GoogleAuthPage (line 21) | function GoogleAuthPage() {
FILE: applications/web/src/routes/(oauth)/auth/outlook.tsx
function OutlookAuthPage (line 21) | function OutlookAuthPage() {
FILE: applications/web/src/routes/(oauth)/auth/route.tsx
function OAuthAuthLayout (line 14) | function OAuthAuthLayout() {
FILE: applications/web/src/routes/(oauth)/dashboard/connect/apple.tsx
function ConnectApplePage (line 8) | function ConnectApplePage() {
FILE: applications/web/src/routes/(oauth)/dashboard/connect/caldav.tsx
function ConnectCalDAVPage (line 10) | function ConnectCalDAVPage() {
FILE: applications/web/src/routes/(oauth)/dashboard/connect/fastmail.tsx
function ConnectFastmailPage (line 10) | function ConnectFastmailPage() {
FILE: applications/web/src/routes/(oauth)/dashboard/connect/ical-link.tsx
function ConnectICalLinkPage (line 14) | function ConnectICalLinkPage() {
FILE: applications/web/src/routes/(oauth)/dashboard/connect/ics-file.tsx
function ConnectICSFilePage (line 14) | function ConnectICSFilePage() {
FILE: applications/web/src/routes/(oauth)/dashboard/connect/route.tsx
function OAuthConnectLayout (line 7) | function OAuthConnectLayout() {
FILE: applications/web/src/routes/(oauth)/dashboard/route.tsx
function OAuthDashboardLayout (line 14) | function OAuthDashboardLayout() {
FILE: applications/web/src/routes/(oauth)/oauth/consent.tsx
type SearchParams (line 19) | type SearchParams = Record<string, string>;
constant SCOPE_LABELS (line 21) | const SCOPE_LABELS: Record<string, string> = {
function extractConsentErrorMessage (line 52) | function extractConsentErrorMessage(payload: unknown): string {
function extractConsentRedirectUrl (line 65) | function extractConsentRedirectUrl(payload: unknown): string {
function McpConsentPage (line 75) | function McpConsentPage() {
FILE: applications/web/src/routes/(oauth)/route.tsx
function OAuthLayout (line 10) | function OAuthLayout() {
FILE: applications/web/src/routes/__root.tsx
constant NON_RETRYABLE_STATUSES (line 14) | const NON_RETRYABLE_STATUSES = new Set([401, 403, 404]);
constant SWR_CONFIG (line 16) | const SWR_CONFIG = {
function ViteScriptTag (line 50) | function ViteScriptTag({ script }: { script: ViteScript }) {
function RootComponent (line 62) | function RootComponent() {
function ScrollToTopOnNavigation (line 110) | function ScrollToTopOnNavigation() {
function NotFound (line 124) | function NotFound() {
function ErrorFallback (line 139) | function ErrorFallback({ error }: ErrorComponentProps) {
FILE: applications/web/src/server.tsx
function render (line 6) | function render(request: Request, viteAssets: ViteAssets): Promise<Respo...
FILE: applications/web/src/server/cache/stale-cache.ts
type CacheEntry (line 1) | interface CacheEntry<TValue> {
type CreateStaleCacheOptions (line 6) | interface CreateStaleCacheOptions<TValue> {
type CacheSnapshot (line 14) | interface CacheSnapshot<TValue> {
type CacheRevalidationPolicy (line 19) | type CacheRevalidationPolicy = "always" | "when-stale";
type CacheState (line 21) | interface CacheState<TValue> {
function createInitialState (line 26) | function createInitialState<TValue>(): CacheState<TValue> {
function isStale (line 33) | function isStale<TValue>(state: CacheState<TValue>, nowMs: number, ttlMs...
function createRefreshTask (line 41) | function createRefreshTask<TValue>(
function shouldRevalidateInBackground (line 53) | function shouldRevalidateInBackground<TValue>(
function createStaleCache (line 70) | function createStaleCache<TValue>(options: CreateStaleCacheOptions<TValu...
FILE: applications/web/src/server/compression.ts
constant COMPRESSIBLE_TYPES (line 3) | const COMPRESSIBLE_TYPES = new Set([
function isCompressible (line 15) | function isCompressible(contentType: string | null): boolean {
function acceptsGzip (line 24) | function acceptsGzip(request: Request): boolean {
function withCompression (line 29) | async function withCompression(
FILE: applications/web/src/server/config.ts
function parsePort (line 11) | function parsePort(variableName: string, value: string): number {
function deriveVitePort (line 20) | function deriveVitePort(serverPort: number): number {
function createServerConfig (line 29) | function createServerConfig(environment: typeof envSchema.infer): Server...
FILE: applications/web/src/server/github-stars.ts
type GithubStarsSnapshot (line 3) | interface GithubStarsSnapshot {
function hasStargazersCount (line 8) | function hasStargazersCount(value: unknown): value is { stargazers_count...
function fetchGithubStarsCount (line 22) | async function fetchGithubStarsCount(): Promise<number> {
function getGithubStarsSnapshot (line 49) | async function getGithubStarsSnapshot(): Promise<GithubStarsSnapshot> {
FILE: applications/web/src/server/http-handler.ts
constant CACHEABLE_PATHS (line 8) | const CACHEABLE_PATHS = new Set(["/", "/blog", "/privacy", "/terms"]);
constant HTML_CACHE_TTL_MS (line 9) | const HTML_CACHE_TTL_MS = 60_000;
type CachedHtml (line 11) | interface CachedHtml {
function getCachedHtml (line 18) | function getCachedHtml(pathname: string): string | null {
function setCachedHtml (line 28) | function setCachedHtml(pathname: string, body: string): void {
function withSecurityHeaders (line 43) | function withSecurityHeaders(response: Response, config: ServerConfig): ...
function resolveGdprCacheSegment (line 60) | function resolveGdprCacheSegment(countryCode: string): string {
function handleApplicationRequest (line 65) | async function handleApplicationRequest(
FILE: applications/web/src/server/internal-routes.ts
constant RESOURCE_SCOPES (line 34) | const RESOURCE_SCOPES = [
function serveStaticTextFile (line 49) | async function serveStaticTextFile(pathname: string): Promise<Response |...
function handleInternalRoute (line 83) | async function handleInternalRoute(
FILE: applications/web/src/server/migration-check.ts
constant MIGRATION_GUIDE_URL (line 1) | const MIGRATION_GUIDE_URL = "https://github.com/ridafkih/keeper.sh/issue...
constant DEPRECATED_ENV_VARS (line 3) | const DEPRECATED_ENV_VARS: Record<string, string> = {
function checkMigrationStatus (line 15) | function checkMigrationStatus(): void {
FILE: applications/web/src/server/proxy/http.ts
function isMcpRequest (line 5) | function isMcpRequest(url: URL): boolean {
function isApiRequest (line 9) | function isApiRequest(url: URL): boolean {
function toProxiedUrl (line 13) | function toProxiedUrl(requestUrl: URL, origin: string): URL {
function proxyRequest (line 18) | async function proxyRequest(request: Request, origin: string): Promise<R...
function resolveStaticFilePath (line 37) | function resolveStaticFilePath(pathname: string): string | null {
function getCacheControlHeader (line 55) | function getCacheControlHeader(pathname: string): string {
function readStaticFile (line 67) | async function readStaticFile(pathname: string): Promise<Response> {
FILE: applications/web/src/server/proxy/websocket.ts
function isSocketProxyPath (line 5) | function isSocketProxyPath(url: URL): boolean {
function isWebSocketUpgradeRequest (line 9) | function isWebSocketUpgradeRequest(request: Request): boolean {
function toWebSocketUrl (line 14) | function toWebSocketUrl(requestUrl: URL, origin: string): string {
function relayUpstreamMessageToClient (line 30) | function relayUpstreamMessageToClient(clientSocket: SocketConnection, me...
function relayClientMessageToUpstream (line 53) | function relayClientMessageToUpstream(upstreamSocket: WebSocket, message...
function upgradeSocketProxy (line 69) | function upgradeSocketProxy(
method close (line 93) | close(clientSocket: SocketConnection, code: number, reason: string): void {
method message (line 110) | message(clientSocket: SocketConnection, message: unknown): void {
method open (line 122) | open(clientSocket: SocketConnection): void {
FILE: applications/web/src/server/runtime.ts
type EntryServerModule (line 10) | interface EntryServerModule {
function isEntryServerModule (line 14) | function isEntryServerModule(value: unknown): value is EntryServerModule {
function loadProductionRenderer (line 23) | async function loadProductionRenderer(): Promise<EntryServerModule> {
function createProductionRuntime (line 32) | async function createProductionRuntime(): Promise<Runtime> {
function createViteDevServerInstance (line 47) | async function createViteDevServerInstance(vitePort: number): Promise<Vi...
function loadDevelopmentRenderer (line 67) | async function loadDevelopmentRenderer(viteServer: ViteDevServer): Promi...
function createDevelopmentRuntime (line 76) | async function createDevelopmentRuntime(vitePort: number): Promise<Runti...
function createRuntime (line 95) | function createRuntime(config: ServerConfig): Promise<Runtime> {
FILE: applications/web/src/server/types.ts
type Runtime (line 4) | interface Runtime {
type ServerConfig (line 11) | interface ServerConfig {
type SocketProxyData (line 20) | interface SocketProxyData {
type SocketServer (line 25) | type SocketServer = BunServer<SocketProxyData>;
type SocketConnection (line 26) | type SocketConnection = BunServerWebSocket<SocketProxyData>;
FILE: applications/web/src/server/vite-assets.ts
type ManifestChunk (line 7) | interface ManifestChunk {
type ViteManifest (line 13) | type ViteManifest = Record<string, ManifestChunk>;
function extractScripts (line 15) | function extractScripts(parent: Element): ViteScript[] {
function readStylesheetContents (line 28) | async function readStylesheetContents(hrefs: string[]): Promise<string[]> {
function collectTransitiveImports (line 44) | function collectTransitiveImports(manifest: ViteManifest, entryKey: stri...
function readManifestPreloads (line 66) | async function readManifestPreloads(): Promise<string[]> {
function extractViteAssets (line 82) | async function extractViteAssets(template: string, inline = false): Prom...
FILE: applications/web/src/state/auth-form.ts
type AuthFormStatus (line 3) | type AuthFormStatus = "idle" | "loading";
type AuthFormStep (line 4) | type AuthFormStep = "email" | "password";
type AuthFormError (line 12) | type AuthFormError = {
FILE: applications/web/src/state/calendar-detail.ts
type ExcludeField (line 20) | type ExcludeField = keyof Pick<
FILE: applications/web/src/state/ical-feed-settings.ts
type FeedSettings (line 4) | interface FeedSettings {
type FeedSettingToggleKey (line 12) | type FeedSettingToggleKey = keyof Omit<FeedSettings, "customEventName">;
constant DEFAULT_FEED_SETTINGS (line 14) | const DEFAULT_FEED_SETTINGS: FeedSettings = {
FILE: applications/web/src/state/sync.ts
type CompositeState (line 5) | type CompositeState = "idle" | "syncing";
type SyncAggregateData (line 7) | type SyncAggregateData = SyncAggregate;
type CompositeSyncState (line 9) | interface CompositeSyncState {
FILE: applications/web/src/types/api.ts
type CalendarAccount (line 1) | interface CalendarAccount {
type CalendarSource (line 16) | interface CalendarSource {
type CalendarDetail (line 33) | interface CalendarDetail {
type ApiEvent (line 57) | interface ApiEvent {
type ApiEventSummary (line 67) | interface ApiEventSummary {
FILE: applications/web/src/utils/calendars.ts
type CalendarLike (line 1) | interface CalendarLike {
FILE: applications/web/src/utils/checkout.ts
type CheckoutCallbacks (line 3) | interface CheckoutCallbacks {
type CheckoutResponse (line 7) | interface CheckoutResponse {
function createCheckoutSession (line 11) | async function createCheckoutSession(productId: string): Promise<Checkou...
function createPortalSession (line 26) | async function createPortalSession(): Promise<CheckoutResponse> {
function openCheckout (line 36) | async function openCheckout(productId: string, callbacks?: CheckoutCallb...
function openCustomerPortal (line 53) | async function openCustomerPortal(): Promise<void> {
FILE: applications/web/src/utils/cn.ts
function cn (line 3) | function cn(...inputs: ClassValue[]) {
FILE: applications/web/src/utils/collections.ts
function resolveUpdatedIds (line 1) | function resolveUpdatedIds(currentIds: string[], targetId: string, check...
FILE: applications/web/src/utils/errors.ts
function resolveErrorMessage (line 1) | function resolveErrorMessage(error: unknown, fallback: string): string {
FILE: applications/web/src/utils/templates.ts
type TemplateSegment (line 1) | type TemplateSegment =
constant TEMPLATE_REGEX (line 5) | const TEMPLATE_REGEX = /\{\{(\w+)\}\}/g;
function parseTemplate (line 7) | function parseTemplate(input: string): TemplateSegment[] {
FILE: applications/web/tests/features/auth/components/auth-form.test.tsx
class FakePublicKeyCredential (line 179) | class FakePublicKeyCredential {}
FILE: applications/web/tests/lib/serialized-mutate.test.ts
function deferred (line 4) | function deferred<T = void>() {
FILE: applications/web/vite.config.ts
method manualChunks (line 42) | manualChunks(id) {
FILE: packages/auth/src/capabilities.ts
type ResolveAuthCapabilitiesConfig (line 4) | interface ResolveAuthCapabilitiesConfig {
FILE: packages/auth/src/index.ts
type EmailUser (line 34) | interface EmailUser {
type SendEmailParams (line 39) | interface SendEmailParams {
type AuthConfig (line 44) | interface AuthConfig {
type KeeperMcpAuthSession (line 63) | interface KeeperMcpAuthSession {
type KeeperMcpAuthApi (line 68) | interface KeeperMcpAuthApi {
type OAuthProviderAuthApi (line 79) | interface OAuthProviderAuthApi {
method onError (line 317) | onError(error: unknown) {
type KeeperMcpEnabledAuth (line 411) | type KeeperMcpEnabledAuth<TAuth = ReturnType<typeof betterAuth>> = TAuth...
type AuthResult (line 430) | type AuthResult = ReturnType<typeof createAuth>;
FILE: packages/auth/src/mcp-config.ts
constant KEEPER_MCP_OAUTH_SCOPES (line 13) | const KEEPER_MCP_OAUTH_SCOPES = [
type ResolveMcpAuthOptionsInput (line 21) | interface ResolveMcpAuthOptionsInput {
constant MCP_ACCESS_TOKEN_EXPIRES_IN (line 26) | const MCP_ACCESS_TOKEN_EXPIRES_IN = 2_592_000;
constant MCP_REFRESH_TOKEN_EXPIRES_IN (line 27) | const MCP_REFRESH_TOKEN_EXPIRES_IN = 7_776_000;
type ResolvedMcpAuthOptions (line 29) | interface ResolvedMcpAuthOptions {
FILE: packages/auth/src/plugins/username-only/endpoints/sign-in.ts
constant INVALID_CREDENTIALS_ERROR (line 6) | const INVALID_CREDENTIALS_ERROR = {
FILE: packages/auth/src/plugins/username-only/types.ts
type User (line 1) | interface User {
type CredentialAccount (line 11) | interface CredentialAccount {
FILE: packages/auth/src/plugins/username-only/utils/config.ts
type UsernameOnlyOptions (line 1) | interface UsernameOnlyOptions {
type UsernameOnlyConfig (line 8) | type UsernameOnlyConfig = Required<UsernameOnlyOptions>;
FILE: packages/auth/src/polar-customer-delete.ts
type PolarCustomerDeletionClient (line 3) | interface PolarCustomerDeletionClient {
FILE: packages/broadcast/src/index.ts
constant EMPTY_CONNECTIONS_COUNT (line 7) | const EMPTY_CONNECTIONS_COUNT = 0;
constant IDLE_TIMEOUT_SECONDS (line 8) | const IDLE_TIMEOUT_SECONDS = 60;
type OnConnectCallback (line 10) | type OnConnectCallback = (userId: string, socket: Socket) => void | Prom...
type WebsocketHandlerOptions (line 12) | interface WebsocketHandlerOptions {
type BroadcastConfig (line 16) | interface BroadcastConfig {
type BroadcastService (line 20) | interface BroadcastService {
constant CHANNEL (line 25) | const CHANNEL = "broadcast";
method close (line 100) | close(socket: Socket): void {
method open (line 106) | async open(socket: Socket): Promise<void> {
FILE: packages/broadcast/src/types.ts
type BroadcastData (line 3) | interface BroadcastData {
type Socket (line 7) | type Socket = ServerWebSocket<BroadcastData>;
FILE: packages/calendar/src/core/events/all-day.ts
type AllDayEventShape (line 1) | interface AllDayEventShape {
constant MS_PER_DAY (line 7) | const MS_PER_DAY = 24 * 60 * 60 * 1000;
FILE: packages/calendar/src/core/events/content-hash.ts
type SyncableEventContent (line 4) | type SyncableEventContent = Pick<SyncableEvent, "summary" | "description...
FILE: packages/calendar/src/core/events/events.ts
constant EMPTY_SOURCES_COUNT (line 16) | const EMPTY_SOURCES_COUNT = 0;
constant TEMPLATE_TOKEN_PATTERN (line 96) | const TEMPLATE_TOKEN_PATTERN = /\{\{(\w+)\}\}/g;
constant DEFAULT_EVENT_NAME (line 97) | const DEFAULT_EVENT_NAME = "Busy";
constant DEFAULT_EVENT_NAME_TEMPLATE (line 98) | const DEFAULT_EVENT_NAME_TEMPLATE = "{{calendar_name}}";
FILE: packages/calendar/src/core/events/mappings.ts
constant DEFAULT_COUNT (line 5) | const DEFAULT_COUNT = 0;
type EventMapping (line 7) | interface EventMapping {
FILE: packages/calendar/src/core/events/recurrence.ts
constant MIN_RECURRENCE_COUNT (line 3) | const MIN_RECURRENCE_COUNT = 0;
constant RECURRENCE_FREQUENCIES (line 5) | const RECURRENCE_FREQUENCIES: IcsRecurrenceRule["frequency"][] = [
constant WEEK_DAYS (line 14) | const WEEK_DAYS: NonNullable<IcsRecurrenceRule["workweekStart"]>[] = [
FILE: packages/calendar/src/core/oauth/accounts.ts
type OAuthAccount (line 14) | interface OAuthAccount {
FILE: packages/calendar/src/core/oauth/config.ts
type OAuthCredentials (line 1) | interface OAuthCredentials {
type OAuthEnv (line 6) | interface OAuthEnv {
type OAuthConfigs (line 13) | interface OAuthConfigs {
FILE: packages/calendar/src/core/oauth/coordinated-refresher.ts
constant MS_PER_SECOND (line 11) | const MS_PER_SECOND = 1000;
type CoordinatedRefresherOptions (line 13) | interface CoordinatedRefresherOptions {
FILE: packages/calendar/src/core/oauth/create-source-provider.ts
constant EMPTY_SOURCES_COUNT (line 8) | const EMPTY_SOURCES_COUNT = 0;
constant INITIAL_ADDED_COUNT (line 9) | const INITIAL_ADDED_COUNT = 0;
constant INITIAL_REMOVED_COUNT (line 10) | const INITIAL_REMOVED_COUNT = 0;
constant INITIAL_INSERTED_COUNT (line 11) | const INITIAL_INSERTED_COUNT = 0;
constant INITIAL_UPDATED_COUNT (line 12) | const INITIAL_UPDATED_COUNT = 0;
constant INITIAL_FILTERED_OUT_OF_WINDOW_COUNT (line 13) | const INITIAL_FILTERED_OUT_OF_WINDOW_COUNT = 0;
constant INITIAL_SYNC_TOKEN_RESET_COUNT (line 14) | const INITIAL_SYNC_TOKEN_RESET_COUNT = 0;
constant SOURCE_SYNC_CONCURRENCY (line 15) | const SOURCE_SYNC_CONCURRENCY = 3;
constant SOURCE_SYNC_TIMEOUT_MS (line 16) | const SOURCE_SYNC_TIMEOUT_MS = 60_000;
type OAuthSourceAccount (line 25) | interface OAuthSourceAccount {
type SourceProvider (line 38) | interface SourceProvider {
type CreateOAuthSourceProviderOptions (line 42) | interface CreateOAuthSourceProviderOptions<
FILE: packages/calendar/src/core/oauth/ensure-valid-token.ts
constant MS_PER_SECOND (line 3) | const MS_PER_SECOND = 1000;
type TokenState (line 5) | interface TokenState {
type OAuthTokenRefreshResult (line 11) | interface OAuthTokenRefreshResult {
type TokenRefresher (line 17) | type TokenRefresher = (refreshToken: string) => Promise<OAuthTokenRefres...
FILE: packages/calendar/src/core/oauth/google.ts
constant GOOGLE_AUTH_URL (line 6) | const GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
constant GOOGLE_TOKEN_URL (line 7) | const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
constant GOOGLE_USERINFO_URL (line 8) | const GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userin...
constant GOOGLE_CALENDAR_SCOPE (line 10) | const GOOGLE_CALENDAR_SCOPE = "https://www.googleapis.com/auth/calendar....
constant GOOGLE_CALENDAR_LIST_SCOPE (line 11) | const GOOGLE_CALENDAR_LIST_SCOPE = "https://www.googleapis.com/auth/cale...
constant GOOGLE_EMAIL_SCOPE (line 12) | const GOOGLE_EMAIL_SCOPE = "https://www.googleapis.com/auth/userinfo.ema...
constant REQUEST_TIMEOUT_MS (line 13) | const REQUEST_TIMEOUT_MS = 15_000;
constant REFRESH_MAX_ATTEMPTS (line 14) | const REFRESH_MAX_ATTEMPTS = 2;
type GoogleOAuthCredentials (line 20) | interface GoogleOAuthCredentials {
type AuthorizationUrlOptions (line 25) | interface AuthorizationUrlOptions {
type GoogleOAuthService (line 32) | interface GoogleOAuthService {
type GoogleTokenErrorPayload (line 38) | interface GoogleTokenErrorPayload {
class GoogleOAuthRefreshError (line 58) | class GoogleOAuthRefreshError extends Error {
method constructor (line 65) | constructor(
FILE: packages/calendar/src/core/oauth/microsoft.ts
constant MICROSOFT_AUTH_URL (line 6) | const MICROSOFT_AUTH_URL = "https://login.microsoftonline.com/common/oau...
constant MICROSOFT_TOKEN_URL (line 7) | const MICROSOFT_TOKEN_URL = "https://login.microsoftonline.com/common/oa...
constant MICROSOFT_USERINFO_URL (line 8) | const MICROSOFT_USERINFO_URL = "https://graph.microsoft.com/v1.0/me";
constant MICROSOFT_CALENDAR_SCOPE (line 10) | const MICROSOFT_CALENDAR_SCOPE = "Calendars.ReadWrite";
constant MICROSOFT_USER_SCOPE (line 11) | const MICROSOFT_USER_SCOPE = "User.Read";
constant MICROSOFT_OFFLINE_SCOPE (line 12) | const MICROSOFT_OFFLINE_SCOPE = "offline_access";
constant REQUEST_TIMEOUT_MS (line 13) | const REQUEST_TIMEOUT_MS = 15_000;
type MicrosoftOAuthCredentials (line 19) | interface MicrosoftOAuthCredentials {
type AuthorizationUrlOptions (line 24) | interface AuthorizationUrlOptions {
type MicrosoftOAuthService (line 31) | interface MicrosoftOAuthService {
FILE: packages/calendar/src/core/oauth/providers.ts
type AuthorizationUrlOptions (line 15) | interface AuthorizationUrlOptions {
type NormalizedUserInfo (line 22) | interface NormalizedUserInfo {
type OAuthTokens (line 27) | interface OAuthTokens {
type OAuthProvider (line 34) | interface OAuthProvider {
type OAuthProvidersConfig (line 42) | interface OAuthProvidersConfig {
type OAuthProviders (line 47) | interface OAuthProviders {
FILE: packages/calendar/src/core/oauth/refresh-coordinator.ts
type CredentialRefreshResult (line 1) | interface CredentialRefreshResult {
type RefreshLockStore (line 7) | interface RefreshLockStore {
constant REFRESH_LOCK_PREFIX (line 12) | const REFRESH_LOCK_PREFIX = "oauth:refresh-lock:";
constant REFRESH_LOCK_TTL_SECONDS (line 13) | const REFRESH_LOCK_TTL_SECONDS = 30;
FILE: packages/calendar/src/core/oauth/source-provider.ts
constant MS_PER_SECOND (line 13) | const MS_PER_SECOND = 1000;
type FetchEventsResult (line 15) | interface FetchEventsResult {
type ProcessEventsOptions (line 23) | interface ProcessEventsOptions {
method constructor (line 37) | constructor(config: TConfig) {
method sync (line 44) | async sync(): Promise<SourceSyncResult> {
method clearSyncToken (line 82) | protected async clearSyncToken(): Promise<void> {
method updateSyncToken (line 90) | protected async updateSyncToken(syncToken: string): Promise<void> {
method markNeedsReauthentication (line 98) | protected async markNeedsReauthentication(): Promise<void> {
method ensureValidToken (line 107) | protected async ensureValidToken(): Promise<void> {
method headers (line 145) | protected get headers(): Record<string, string> {
FILE: packages/calendar/src/core/oauth/state.ts
constant STATE_EXPIRY_MINUTES (line 3) | const STATE_EXPIRY_MINUTES = 10;
constant STATE_PREFIX (line 4) | const STATE_PREFIX = "oauth:state:";
constant MS_PER_SECOND (line 5) | const MS_PER_SECOND = 1000;
type PendingState (line 7) | interface PendingState {
type ValidatedState (line 14) | interface ValidatedState {
type GenerateStateOptions (line 20) | interface GenerateStateOptions {
type OAuthStateStore (line 25) | interface OAuthStateStore {
constant STATE_EXPIRY_SECONDS (line 32) | const STATE_EXPIRY_SECONDS = STATE_EXPIRY_MINUTES * MS_PER_MINUTE / MS_P...
FILE: packages/calendar/src/core/oauth/sync-token.ts
constant VERSIONED_SYNC_TOKEN_PREFIX (line 1) | const VERSIONED_SYNC_TOKEN_PREFIX = "keeper:sync-token:";
constant LEGACY_SYNC_WINDOW_VERSION (line 2) | const LEGACY_SYNC_WINDOW_VERSION = 0;
type DecodedSyncToken (line 4) | interface DecodedSyncToken {
type ResolvedSyncToken (line 9) | interface ResolvedSyncToken {
FILE: packages/calendar/src/core/oauth/sync-window.ts
constant OAUTH_SYNC_LOOKBACK_DAYS (line 9) | const OAUTH_SYNC_LOOKBACK_DAYS = 7;
constant OAUTH_SYNC_WINDOW_VERSION (line 10) | const OAUTH_SYNC_WINDOW_VERSION = 3;
constant OAUTH_SYNC_LOOKBACK_MS (line 11) | const OAUTH_SYNC_LOOKBACK_MS = OAUTH_SYNC_LOOKBACK_DAYS * MS_PER_DAY;
type OAuthSyncWindow (line 13) | interface OAuthSyncWindow {
FILE: packages/calendar/src/core/oauth/token-provider.ts
type OAuthRefreshResult (line 1) | interface OAuthRefreshResult {
type OAuthTokenProvider (line 7) | interface OAuthTokenProvider {
FILE: packages/calendar/src/core/source/event-diff.ts
type ExistingSourceEventState (line 3) | interface ExistingSourceEventState {
type SourceEventDiffOptions (line 13) | interface SourceEventDiffOptions {
type SourceEventIdentityOptions (line 18) | interface SourceEventIdentityOptions {
FILE: packages/calendar/src/core/source/sync-diagnostics.ts
type OAuthSyncWindow (line 4) | interface OAuthSyncWindow {
type SourceEventsInWindowResult (line 9) | interface SourceEventsInWindowResult {
type SourceEventStoragePartition (line 14) | interface SourceEventStoragePartition {
type SourceSyncTokenAction (line 19) | interface SourceSyncTokenAction {
FILE: packages/calendar/src/core/source/write-event-states.ts
constant EMPTY_ROW_COUNT (line 4) | const EMPTY_ROW_COUNT = 0;
type EventStateInsertRow (line 6) | type EventStateInsertRow = typeof eventStatesTable.$inferInsert;
constant EVENT_STATE_CONFLICT_TARGET (line 8) | const EVENT_STATE_CONFLICT_TARGET: [
constant EVENT_STATE_CONFLICT_SET (line 22) | const EVENT_STATE_CONFLICT_SET = {
type EventStateInsertClient (line 34) | interface EventStateInsertClient {
FILE: packages/calendar/src/core/sync-engine/flush.ts
constant FLUSH_BATCH_SIZE (line 6) | const FLUSH_BATCH_SIZE = 5000;
FILE: packages/calendar/src/core/sync-engine/generation.ts
type GenerationStore (line 1) | interface GenerationStore {
constant GENERATION_PREFIX (line 7) | const GENERATION_PREFIX = "sync:gen:";
constant GENERATION_TTL_SECONDS (line 8) | const GENERATION_TTL_SECONDS = 86_400;
FILE: packages/calendar/src/core/sync-engine/index.ts
type OperationError (line 18) | interface OperationError {
type ExecuteRemoteResult (line 101) | interface ExecuteRemoteResult {
type OperationRun (line 109) | interface OperationRun {
type RunResult (line 135) | interface RunResult {
type ProgressCallback (line 187) | type ProgressCallback = (processed: number, total: number) => void;
constant OPERATION_CHUNK_SIZE (line 189) | const OPERATION_CHUNK_SIZE = 50;
type ChunkedExecutionState (line 199) | interface ChunkedExecutionState {
type SyncCalendarOptions (line 308) | interface SyncCalendarOptions {
type SyncCalendarResult (line 324) | interface SyncCalendarResult extends SyncResult {
constant EMPTY_RESULT (line 329) | const EMPTY_RESULT: SyncCalendarResult = { added: 0, addFailed: 0, remov...
FILE: packages/calendar/src/core/sync-engine/ingest.ts
type ExistingEventState (line 4) | interface ExistingEventState {
type FetchEventsResult (line 14) | interface FetchEventsResult {
type IngestionChanges (line 23) | interface IngestionChanges {
type IngestSourceOptions (line 29) | interface IngestSourceOptions {
type IngestionResult (line 37) | interface IngestionResult {
constant EMPTY_RESULT (line 42) | const EMPTY_RESULT: IngestionResult = { eventsAdded: 0, eventsRemoved: 0 };
FILE: packages/calendar/src/core/sync-engine/types.ts
type CalendarSyncProvider (line 3) | interface CalendarSyncProvider {
type PendingInsert (line 9) | interface PendingInsert {
type PendingChanges (line 19) | interface PendingChanges {
FILE: packages/calendar/src/core/sync/aggregate-runtime.ts
constant SYNC_AGGREGATE_LATEST_KEY_PREFIX (line 7) | const SYNC_AGGREGATE_LATEST_KEY_PREFIX = "sync:aggregate:latest:";
constant SYNC_AGGREGATE_SEQUENCE_KEY_PREFIX (line 8) | const SYNC_AGGREGATE_SEQUENCE_KEY_PREFIX = "sync:aggregate:seq:";
type SyncAggregateRuntimeConfig (line 10) | interface SyncAggregateRuntimeConfig {
type SyncAggregateRuntime (line 17) | interface SyncAggregateRuntime {
FILE: packages/calendar/src/core/sync/aggregate-tracker.ts
type CalendarOperationProgress (line 3) | interface CalendarOperationProgress {
type SyncAggregateSnapshot (line 9) | interface SyncAggregateSnapshot {
type SyncAggregateMessage (line 18) | interface SyncAggregateMessage extends SyncAggregateSnapshot {
type SyncAggregateTrackerConfig (line 22) | interface SyncAggregateTrackerConfig {
constant DEFAULT_PROGRESS_THROTTLE_MS (line 26) | const DEFAULT_PROGRESS_THROTTLE_MS = 350;
constant INITIAL_COUNT (line 27) | const INITIAL_COUNT = 0;
constant INITIAL_SEQUENCE (line 28) | const INITIAL_SEQUENCE = 0;
constant PERCENT_MULTIPLIER (line 29) | const PERCENT_MULTIPLIER = 100;
class SyncAggregateTracker (line 31) | class SyncAggregateTracker {
method constructor (line 40) | constructor(config?: SyncAggregateTrackerConfig) {
method getUserProgress (line 44) | private getUserProgress(userId: string): Map<string, CalendarOperation...
method clampProgress (line 55) | private static clampProgress(processed: number, total: number): number {
method calculatePercent (line 65) | private static calculatePercent(processed: number, total: number, sync...
method mergeProgressEntry (line 75) | private static mergeProgressEntry(
method finalizeEntry (line 101) | private static finalizeEntry(
method computeSnapshot (line 117) | private computeSnapshot(
method toPayloadKey (line 177) | private static toPayloadKey(payload: SyncAggregateSnapshot): string {
method getCurrentSequence (line 181) | private getCurrentSequence(userId: string): number {
method nextSequence (line 185) | private nextSequence(userId: string): number {
method maybeEmit (line 191) | private maybeEmit(
method trackProgress (line 225) | trackProgress(update: SyncProgressUpdate): SyncAggregateMessage | null {
method trackDestinationSync (line 240) | trackDestinationSync(
method holdSyncing (line 252) | holdSyncing(userId: string): void {
method releaseSyncing (line 256) | releaseSyncing(userId: string): void {
method getCurrentAggregate (line 260) | getCurrentAggregate(
FILE: packages/calendar/src/core/sync/operations.ts
type RemoveOperationTimeBoundary (line 6) | interface RemoveOperationTimeBoundary {
type StaleMappingResult (line 11) | interface StaleMappingResult {
type ComputeSyncOperationsResult (line 17) | interface ComputeSyncOperationsResult {
FILE: packages/calendar/src/core/sync/types.ts
type DestinationSyncResult (line 1) | interface DestinationSyncResult {
type SyncStage (line 9) | type SyncStage = "fetching" | "comparing" | "processing" | "error";
type SyncProgressUpdate (line 11) | interface SyncProgressUpdate {
FILE: packages/calendar/src/core/types.ts
type AuthType (line 4) | type AuthType = "oauth" | "caldav" | "none";
type EventAvailability (line 5) | type EventAvailability = "busy" | "free" | "oof" | "workingElsewhere";
type SourceEventType (line 6) | type SourceEventType = "default" | "focusTime" | "outOfOffice" | "workin...
type SourcePreferenceOption (line 8) | interface SourcePreferenceOption {
type SourcePreferencesConfig (line 16) | interface SourcePreferencesConfig {
type CalDAVProviderConfig (line 22) | interface CalDAVProviderConfig {
type ProviderCapabilities (line 30) | interface ProviderCapabilities {
type ProviderDefinition (line 35) | interface ProviderDefinition {
type SyncableEvent (line 46) | interface SyncableEvent {
type PushResult (line 64) | interface PushResult {
type DeleteResult (line 73) | interface DeleteResult {
type SyncResult (line 79) | interface SyncResult {
type RemoteEvent (line 86) | interface RemoteEvent {
type SyncOperation (line 94) | type SyncOperation =
type ListRemoteEventsOptions (line 98) | interface ListRemoteEventsOptions {
type BroadcastSyncStatus (line 102) | type BroadcastSyncStatus = (
type ProviderConfig (line 108) | interface ProviderConfig {
type OAuthProviderConfig (line 115) | interface OAuthProviderConfig extends ProviderConfig {
type GoogleCalendarConfig (line 123) | interface GoogleCalendarConfig extends OAuthProviderConfig {
type OutlookCalendarConfig (line 127) | type OutlookCalendarConfig = OAuthProviderConfig;
type CalDAVConfig (line 129) | interface CalDAVConfig extends ProviderConfig {
type SourceEvent (line 135) | interface SourceEvent {
type SourceSyncResult (line 150) | interface SourceSyncResult {
type OAuthSourceConfig (line 162) | interface OAuthSourceConfig {
FILE: packages/calendar/src/core/utils/concurrency.ts
constant DEFAULT_CONCURRENCY (line 1) | const DEFAULT_CONCURRENCY = 5;
type AllSettledWithConcurrencyOptions (line 3) | interface AllSettledWithConcurrencyOptions {
FILE: packages/calendar/src/core/utils/rate-limiter.ts
type QueuedTask (line 1) | type QueuedTask = () => Promise<void>;
constant INITIAL_BACKOFF_MS (line 3) | const INITIAL_BACKOFF_MS = 1000;
constant MAX_BACKOFF_MS (line 4) | const MAX_BACKOFF_MS = 60_000;
constant BACKOFF_MULTIPLIER (line 5) | const BACKOFF_MULTIPLIER = 2;
constant DEFAULT_CONCURRENCY (line 6) | const DEFAULT_CONCURRENCY = 5;
constant DEFAULT_REQUESTS_PER_MINUTE (line 7) | const DEFAULT_REQUESTS_PER_MINUTE = 600;
constant MS_PER_MINUTE (line 8) | const MS_PER_MINUTE = 60_000;
constant INITIAL_ACTIVE_COUNT (line 9) | const INITIAL_ACTIVE_COUNT = 0;
constant INITIAL_BACKOFF_UNTIL (line 10) | const INITIAL_BACKOFF_UNTIL = 0;
constant EMPTY_QUEUE_LENGTH (line 11) | const EMPTY_QUEUE_LENGTH = 0;
constant MIN_DELAY (line 12) | const MIN_DELAY = 0;
constant FIRST_TIMESTAMP_INDEX (line 13) | const FIRST_TIMESTAMP_INDEX = 0;
type RateLimiterConfig (line 15) | interface RateLimiterConfig {
class RateLimiter (line 20) | class RateLimiter {
method constructor (line 33) | constructor(config: RateLimiterConfig = {}) {
method execute (line 39) | execute<TResult>(operation: () => Promise<TResult>): Promise<TResult> {
method reportRateLimit (line 58) | reportRateLimit(): void {
method resetBackoff (line 64) | private resetBackoff(): void {
method getFirstTimestamp (line 70) | private getFirstTimestamp(): number | null {
method pruneOldTimestamps (line 77) | private pruneOldTimestamps(): void {
method canMakeRequest (line 86) | private canMakeRequest(): boolean {
method getDelayUntilNextSlot (line 91) | private getDelayUntilNextSlot(): number {
method recordRequest (line 102) | private recordRequest(): void {
method scheduleQueueProcessing (line 106) | private scheduleQueueProcessing(): void {
method processQueue (line 116) | private processQueue(): void {
method executeTask (line 152) | private async executeTask(task: QueuedTask): Promise<void> {
FILE: packages/calendar/src/core/utils/redis-rate-limiter.ts
constant MS_PER_MINUTE (line 3) | const MS_PER_MINUTE = 60_000;
constant RETRY_POLL_MS (line 4) | const RETRY_POLL_MS = 100;
type RedisRateLimiter (line 6) | interface RedisRateLimiter {
type RedisRateLimiterConfig (line 10) | interface RedisRateLimiterConfig {
constant ACQUIRE_SCRIPT (line 27) | const ACQUIRE_SCRIPT = `
FILE: packages/calendar/src/ics/utils/create-snapshot.ts
type CreateSnapshotResult (line 18) | interface CreateSnapshotResult {
FILE: packages/calendar/src/ics/utils/diff-events.ts
constant EMPTY_SERIALIZED_VALUE (line 4) | const EMPTY_SERIALIZED_VALUE = "";
constant MS_PER_DAY (line 5) | const MS_PER_DAY = 24 * 60 * 60 * 1000;
FILE: packages/calendar/src/ics/utils/fetch-adapter.ts
type IcsSourceFetcherConfig (line 10) | interface IcsSourceFetcherConfig {
type IcsSourceFetcher (line 17) | interface IcsSourceFetcher {
FILE: packages/calendar/src/ics/utils/normalize-timezone.ts
constant WINDOWS_TO_IANA (line 10) | const WINDOWS_TO_IANA: Record<string, string> = {
FILE: packages/calendar/src/ics/utils/parse-ics-calendar.ts
type ParseIcsCalendarOptions (line 3) | interface ParseIcsCalendarOptions {
FILE: packages/calendar/src/ics/utils/parse-ics-events.ts
constant DEFAULT_DURATION_VALUE (line 13) | const DEFAULT_DURATION_VALUE = 0;
FILE: packages/calendar/src/ics/utils/pull-remote-calendar.ts
class CalendarFetchError (line 14) | class CalendarFetchError extends Error {
method constructor (line 18) | constructor(
type ParsedUrl (line 30) | interface ParsedUrl {
constant ICS_USER_AGENT (line 49) | const ICS_USER_AGENT = "Keeper/1.0 (+https://www.keeper.sh)";
type ParsedCalendarResult (line 82) | type ParsedCalendarResult = ReturnType<typeof parseIcsCalendar>;
type OutputICal (line 84) | type OutputICal = "ical" | ["ical"];
type OutputJSON (line 85) | type OutputJSON = "json" | ["json"];
type OutputICALOrJSON (line 86) | type OutputICALOrJSON = ["ical", "json"] | ["json", "ical"];
type JustICal (line 88) | interface JustICal {
type JustJSON (line 92) | interface JustJSON {
type ICalOrJSON (line 96) | type ICalOrJSON = Omit<JustICal, "json"> & Omit<JustJSON, "ical">;
function pullRemoteCalendar (line 114) | async function pullRemoteCalendar(
FILE: packages/calendar/src/ics/utils/types.ts
type EventAvailability (line 1) | type EventAvailability = "busy" | "free" | "oof" | "workingElsewhere";
type EventTimeSlot (line 3) | interface EventTimeSlot {
type StoredEventTimeSlot (line 17) | type StoredEventTimeSlot = EventTimeSlot & {
type EventDiff (line 21) | interface EventDiff {
type SerializedIcsCalendar (line 26) | interface SerializedIcsCalendar {
FILE: packages/calendar/src/providers/caldav/destination/provider.ts
constant CALDAV_RATE_LIMIT_CONCURRENCY (line 9) | const CALDAV_RATE_LIMIT_CONCURRENCY = 5;
constant YEARS_UNTIL_FUTURE (line 10) | const YEARS_UNTIL_FUTURE = 2;
type CalDAVSyncProviderConfig (line 12) | interface CalDAVSyncProviderConfig {
FILE: packages/calendar/src/providers/caldav/shared/client.ts
type CalendarObject (line 8) | interface CalendarObject {
type DAVClientInstance (line 14) | type DAVClientInstance = Awaited<ReturnType<typeof createDAVClient>>;
class CalDAVClient (line 23) | class CalDAVClient {
method constructor (line 29) | constructor(config: CalDAVClientConfig, safeFetchOptions?: SafeFetchOp...
method getResolvedAuthMethod (line 34) | getResolvedAuthMethod(): CalDAVAuthMethod | null {
method getClient (line 38) | private async getClient(): Promise<DAVClientInstance> {
method discoverCalendars (line 59) | async discoverCalendars(): Promise<CalendarInfo[]> {
method fetchCalendarDisplayName (line 72) | async fetchCalendarDisplayName(calendarUrl: string): Promise<string | ...
method resolveCalendarUrl (line 83) | async resolveCalendarUrl(storedUrl: string): Promise<string> {
method createCalendarObject (line 94) | async createCalendarObject(params: {
method deleteCalendarObject (line 110) | async deleteCalendarObject(params: { calendarUrl: string; filename: st...
method fetchCalendarObjects (line 121) | async fetchCalendarObjects(params: {
method ensureTrailingSlash (line 135) | private static ensureTrailingSlash(url: string): string {
method normalizeUrl (line 143) | private static normalizeUrl(calendarUrl: string, filename: string): st...
FILE: packages/calendar/src/providers/caldav/shared/digest-fetch.ts
type FetchFunction (line 3) | type FetchFunction = (input: string | Request | URL, init?: RequestInit)...
type AuthMethod (line 5) | type AuthMethod = "unknown" | "basic" | "digest";
constant HTTP_UNAUTHORIZED (line 7) | const HTTP_UNAUTHORIZED = 401;
type DigestFetchCredentials (line 9) | interface DigestFetchCredentials {
type DigestAwareFetchOptions (line 42) | interface DigestAwareFetchOptions {
type DigestAwareFetchResult (line 48) | interface DigestAwareFetchResult {
type CalDAVAuthMethod (line 102) | type CalDAVAuthMethod = "basic" | "digest";
FILE: packages/calendar/src/providers/caldav/shared/ics.ts
constant DEFAULT_DURATION_VALUE (line 15) | const DEFAULT_DURATION_VALUE = 0;
type ParsedCalendarEvent (line 69) | interface ParsedCalendarEvent {
FILE: packages/calendar/src/providers/caldav/shared/sync-window.ts
type CalDAVSyncWindow (line 3) | interface CalDAVSyncWindow {
FILE: packages/calendar/src/providers/caldav/source/auth-error-classification.ts
constant AUTH_ERROR_STATUS_CODES (line 1) | const AUTH_ERROR_STATUS_CODES = new Set([401]);
constant AUTH_ERROR_PATTERNS (line 3) | const AUTH_ERROR_PATTERNS = [
FILE: packages/calendar/src/providers/caldav/source/fetch-adapter.ts
constant YEARS_UNTIL_FUTURE (line 9) | const YEARS_UNTIL_FUTURE = 2;
type CalDAVSourceFetcherConfig (line 11) | interface CalDAVSourceFetcherConfig {
type CalDAVSourceFetcher (line 20) | interface CalDAVSourceFetcher {
FILE: packages/calendar/src/providers/caldav/source/provider.ts
constant EMPTY_COUNT (line 27) | const EMPTY_COUNT = 0;
constant YEARS_UNTIL_FUTURE (line 28) | const YEARS_UNTIL_FUTURE = 2;
constant DEFAULT_CALDAV_OPTIONS (line 30) | const DEFAULT_CALDAV_OPTIONS: CalDAVProviderOptions = {
type CalDAVSourceProvider (line 35) | interface CalDAVSourceProvider {
FILE: packages/calendar/src/providers/caldav/source/sync.ts
constant CALDAV_CALENDAR_TYPE (line 6) | const CALDAV_CALENDAR_TYPE = "caldav";
type CalDAVSourceService (line 8) | interface CalDAVSourceService {
FILE: packages/calendar/src/providers/caldav/types.ts
type CalDAVProviderOptions (line 4) | interface CalDAVProviderOptions {
type CalDAVProviderConfig (line 9) | interface CalDAVProviderConfig {
type CalDAVAccount (line 14) | interface CalDAVAccount {
type CalDAVSourceAccount (line 26) | interface CalDAVSourceAccount {
type CalDAVSourceConfig (line 41) | interface CalDAVSourceConfig {
type CalDAVSourceProviderConfig (line 50) | interface CalDAVSourceProviderConfig {
type CalDAVSourceSyncResult (line 55) | interface CalDAVSourceSyncResult {
type CalDAVServiceConfig (line 61) | interface CalDAVServiceConfig {
type CalDAVService (line 66) | interface CalDAVService {
type CalDAVClientConfig (line 73) | interface CalDAVClientConfig {
type CalendarInfo (line 82) | interface CalendarInfo {
FILE: packages/calendar/src/providers/fastmail/destination/provider.ts
constant FASTMAIL_SERVER_URL (line 1) | const FASTMAIL_SERVER_URL = "https://caldav.fastmail.com/";
FILE: packages/calendar/src/providers/fastmail/source/provider.ts
constant PROVIDER_OPTIONS (line 4) | const PROVIDER_OPTIONS = {
FILE: packages/calendar/src/providers/google/destination/provider.ts
type GoogleSyncProviderConfig (line 17) | interface GoogleSyncProviderConfig {
class GoogleCalendarApiError (line 29) | class GoogleCalendarApiError extends Error {
method constructor (line 32) | constructor(status: number, body: string) {
type ConflictEntry (line 93) | interface ConflictEntry { index: number; uid: string; event: SyncableEve...
FILE: packages/calendar/src/providers/google/destination/sync.ts
constant PROVIDER (line 7) | const PROVIDER = "google";
type GoogleAccount (line 9) | type GoogleAccount = OAuthAccount;
FILE: packages/calendar/src/providers/google/shared/api.ts
constant GOOGLE_CALENDAR_API (line 1) | const GOOGLE_CALENDAR_API = "https://www.googleapis.com/calendar/v3/";
constant GOOGLE_CALENDAR_EVENTS_URL (line 2) | const GOOGLE_CALENDAR_EVENTS_URL = "https://www.googleapis.com/calendar/...
constant GOOGLE_CALENDAR_LIST_URL (line 3) | const GOOGLE_CALENDAR_LIST_URL = "https://www.googleapis.com/calendar/v3...
constant GOOGLE_CALENDAR_MAX_RESULTS (line 4) | const GOOGLE_CALENDAR_MAX_RESULTS = 250;
constant GONE_STATUS (line 5) | const GONE_STATUS = 410;
constant GOOGLE_BATCH_API (line 6) | const GOOGLE_BATCH_API = "https://www.googleapis.com/batch/calendar/v3";
constant GOOGLE_BATCH_MAX_SIZE (line 7) | const GOOGLE_BATCH_MAX_SIZE = 50;
FILE: packages/calendar/src/providers/google/shared/backoff.ts
constant MAX_JITTER_MS (line 1) | const MAX_JITTER_MS = 1000;
constant MAX_BACKOFF_MS (line 2) | const MAX_BACKOFF_MS = 64_000;
constant DEFAULT_MAX_RETRIES (line 3) | const DEFAULT_MAX_RETRIES = 5;
constant BACKOFF_MULTIPLIER (line 4) | const BACKOFF_MULTIPLIER = 2;
type BackoffOptions (line 47) | interface BackoffOptions {
FILE: packages/calendar/src/providers/google/shared/batch.ts
type BatchSubRequest (line 7) | interface BatchSubRequest {
type BatchSubResponse (line 14) | interface BatchSubResponse {
constant DEFAULT_SEPARATOR_LENGTH (line 144) | const DEFAULT_SEPARATOR_LENGTH = 2;
class GoogleBatchApiError (line 216) | class GoogleBatchApiError extends Error {
method constructor (line 219) | constructor(status: number, body: string) {
type BatchChunkedOptions (line 284) | interface BatchChunkedOptions {
FILE: packages/calendar/src/providers/google/shared/errors.ts
constant GOOGLE_PERMISSION_REAUTH_REASONS (line 5) | const GOOGLE_PERMISSION_REAUTH_REASONS = new Set([
constant GOOGLE_FORBIDDEN_AUTH_REASONS (line 10) | const GOOGLE_FORBIDDEN_AUTH_REASONS = new Set([
constant RATE_LIMIT_REASONS (line 71) | const RATE_LIMIT_REASONS = new Set([
constant EMPTY_API_ERROR (line 96) | const EMPTY_API_ERROR: GoogleApiError = {};
FILE: packages/calendar/src/providers/google/source/fetch-adapter.ts
constant YEARS_UNTIL_FUTURE (line 7) | const YEARS_UNTIL_FUTURE = 2;
type GoogleSourceFetcherConfig (line 9) | interface GoogleSourceFetcherConfig {
type GoogleSourceFetcher (line 16) | interface GoogleSourceFetcher {
FILE: packages/calendar/src/providers/google/source/provider.ts
constant GOOGLE_PROVIDER_ID (line 22) | const GOOGLE_PROVIDER_ID = "google";
constant EMPTY_COUNT (line 23) | const EMPTY_COUNT = 0;
constant YEARS_UNTIL_FUTURE (line 31) | const YEARS_UNTIL_FUTURE = 2;
type GoogleSourceConfig (line 33) | interface GoogleSourceConfig extends OAuthSourceConfig {
class GoogleCalendarSourceProvider (line 40) | class GoogleCalendarSourceProvider extends OAuthSourceProvider<GoogleSou...
method constructor (line 46) | constructor(config: GoogleSourceConfig, oauthProvider: OAuthTokenProvi...
method fetchEvents (line 51) | async fetchEvents(syncToken: string | null): Promise<BaseFetchEventsRe...
method processEvents (line 94) | protected async processEvents(
method removeOutOfRangeEvents (line 193) | private static async removeOutOfRangeEvents(
type GoogleSourceAccount (line 213) | interface GoogleSourceAccount {
type CreateGoogleSourceProviderConfig (line 231) | interface CreateGoogleSourceProviderConfig {
FILE: packages/calendar/src/providers/google/source/types.ts
type GoogleCalendarListEntry (line 3) | interface GoogleCalendarListEntry {
type GoogleCalendarListResponse (line 13) | interface GoogleCalendarListResponse {
type GoogleEventDateTime (line 19) | interface GoogleEventDateTime {
type GoogleCalendarEvent (line 25) | interface GoogleCalendarEvent {
type GoogleEventsListResponse (line 45) | interface GoogleEventsListResponse {
type FetchEventsOptions (line 52) | interface FetchEventsOptions {
type FetchEventsResult (line 62) | interface FetchEventsResult {
type EventTimeSlot (line 70) | interface EventTimeSlot {
FILE: packages/calendar/src/providers/google/source/utils/fetch-events.ts
constant EMPTY_API_ERROR (line 21) | const EMPTY_API_ERROR: GoogleApiError = {};
class EventsFetchError (line 35) | class EventsFetchError extends Error {
method constructor (line 40) | constructor(
constant REQUEST_TIMEOUT_MS (line 54) | const REQUEST_TIMEOUT_MS = 15_000;
type PageFetchOptions (line 60) | interface PageFetchOptions {
type PageFetchResult (line 70) | interface PageFetchResult {
type FullSyncRequiredResult (line 75) | interface FullSyncRequiredResult {
FILE: packages/calendar/src/providers/google/source/utils/list-calendars.ts
class CalendarListError (line 6) | class CalendarListError extends Error {
method constructor (line 10) | constructor(
FILE: packages/calendar/src/providers/google/types.ts
type GoogleDateTime (line 1) | interface GoogleDateTime {
type PartialGoogleDateTime (line 7) | interface PartialGoogleDateTime {
type GoogleApiError (line 13) | interface GoogleApiError {
FILE: packages/calendar/src/providers/icloud/destination/provider.ts
constant ICLOUD_SERVER_URL (line 1) | const ICLOUD_SERVER_URL = "https://caldav.icloud.com/";
FILE: packages/calendar/src/providers/icloud/source/provider.ts
constant PROVIDER_OPTIONS (line 4) | const PROVIDER_OPTIONS = {
FILE: packages/calendar/src/providers/outlook/destination/provider.ts
type OutlookSyncProviderConfig (line 16) | interface OutlookSyncProviderConfig {
FILE: packages/calendar/src/providers/outlook/destination/sync.ts
constant PROVIDER (line 7) | const PROVIDER = "outlook";
type OutlookAccount (line 9) | type OutlookAccount = OAuthAccount;
FILE: packages/calendar/src/providers/outlook/shared/api.ts
constant MICROSOFT_GRAPH_API (line 1) | const MICROSOFT_GRAPH_API = "https://graph.microsoft.com/v1.0";
constant OUTLOOK_PAGE_SIZE (line 2) | const OUTLOOK_PAGE_SIZE = 100;
constant GONE_STATUS (line 3) | const GONE_STATUS = 410;
FILE: packages/calendar/src/providers/outlook/source/fetch-adapter.ts
constant YEARS_UNTIL_FUTURE (line 6) | const YEARS_UNTIL_FUTURE = 2;
constant OUTLOOK_SYNC_TOKEN_VERSION (line 7) | const OUTLOOK_SYNC_TOKEN_VERSION = OAUTH_SYNC_WINDOW_VERSION + 1;
type OutlookSourceFetcherConfig (line 9) | interface OutlookSourceFetcherConfig {
type OutlookSourceFetcher (line 15) | interface OutlookSourceFetcher {
FILE: packages/calendar/src/providers/outlook/source/provider.ts
constant OUTLOOK_PROVIDER_ID (line 22) | const OUTLOOK_PROVIDER_ID = "outlook";
constant EMPTY_COUNT (line 23) | const EMPTY_COUNT = 0;
constant OUTLOOK_SYNC_TOKEN_VERSION (line 24) | const OUTLOOK_SYNC_TOKEN_VERSION = OAUTH_SYNC_WINDOW_VERSION + 1;
constant YEARS_UNTIL_FUTURE (line 32) | const YEARS_UNTIL_FUTURE = 2;
type OutlookSourceConfig (line 34) | interface OutlookSourceConfig extends OAuthSourceConfig {
class OutlookSourceProvider (line 40) | class OutlookSourceProvider extends OAuthSourceProvider<OutlookSourceCon...
method constructor (line 46) | constructor(config: OutlookSourceConfig, oauthProvider: OAuthTokenProv...
method fetchEvents (line 51) | async fetchEvents(syncToken: string | null): Promise<BaseFetchEventsRe...
method processEvents (line 96) | protected async processEvents(
method refreshOriginalName (line 199) | private async refreshOriginalName(): Promise<void> {
method removeOutOfRangeEvents (line 217) | private static async removeOutOfRangeEvents(
type OutlookSourceAccount (line 237) | interface OutlookSourceAccount {
type CreateOutlookSourceProviderConfig (line 253) | interface CreateOutlookSourceProviderConfig {
FILE: packages/calendar/src/providers/outlook/source/types.ts
type OutlookCalendarListEntry (line 1) | interface OutlookCalendarListEntry {
type OutlookCalendarListResponse (line 13) | interface OutlookCalendarListResponse {
type OutlookEventDateTime (line 18) | interface OutlookEventDateTime {
type OutlookRemovedInfo (line 23) | interface OutlookRemovedInfo {
type OutlookCalendarEvent (line 27) | interface OutlookCalendarEvent {
type OutlookEventsListResponse (line 43) | interface OutlookEventsListResponse {
type FetchEventsOptions (line 49) | interface FetchEventsOptions {
type FetchEventsResult (line 57) | interface FetchEventsResult {
type EventTimeSlot (line 65) | interface EventTimeSlot {
FILE: packages/calendar/src/providers/outlook/source/utils/fetch-events.ts
class EventsFetchError (line 15) | class EventsFetchError extends Error {
method constructor (line 19) | constructor(
constant REQUEST_TIMEOUT_MS (line 31) | const REQUEST_TIMEOUT_MS = 30_000;
constant DEFAULT_PAGE_SIZE (line 32) | const DEFAULT_PAGE_SIZE = 50;
type PageFetchOptions (line 38) | interface PageFetchOptions {
type PageFetchResult (line 47) | interface PageFetchResult {
type FullSyncRequiredResult (line 52) | interface FullSyncRequiredResult {
type FetchCalendarNameOptions (line 56) | interface FetchCalendarNameOptions {
FILE: packages/calendar/src/providers/outlook/source/utils/list-calendars.ts
constant INVALID_RESPONSE_STATUS (line 5) | const INVALID_RESPONSE_STATUS = 502;
class CalendarListError (line 7) | class CalendarListError extends Error {
method constructor (line 11) | constructor(
FILE: packages/calendar/src/providers/outlook/types.ts
type OutlookDateTime (line 1) | interface OutlookDateTime {
type PartialOutlookDateTime (line 6) | interface PartialOutlookDateTime {
type MicrosoftApiError (line 11) | interface MicrosoftApiError {
FILE: packages/calendar/src/utils/registry/registry.ts
constant PROVIDER_DEFINITIONS (line 93) | const PROVIDER_DEFINITIONS = [
type ProviderId (line 102) | type ProviderId = (typeof PROVIDER_DEFINITIONS)[number]["id"];
type OAuthProviderDefinition (line 104) | type OAuthProviderDefinition = Extract<
type CalDAVProviderDefinition (line 109) | type CalDAVProviderDefinition = Extract<
type OAuthProviderId (line 114) | type OAuthProviderId = OAuthProviderDefinition["id"];
type CalDAVProviderId (line 115) | type CalDAVProviderId = CalDAVProviderDefinition["id"];
FILE: packages/calendar/src/utils/registry/server.ts
type SourceFactoryConfig (line 12) | interface SourceFactoryConfig {
type SourceFactory (line 18) | type SourceFactory = (config: SourceFactoryConfig) => SourceProvider;
constant SOURCE_OAUTH_FACTORIES (line 20) | const SOURCE_OAUTH_FACTORIES: Record<string, SourceFactory> = {
type SourceProvidersConfig (line 25) | interface SourceProvidersConfig {
FILE: packages/calendar/src/utils/safe-fetch.ts
constant ALLOWED_PROTOCOLS (line 5) | const ALLOWED_PROTOCOLS = new Set(["http:", "https:"]);
constant MAX_REDIRECTS (line 6) | const MAX_REDIRECTS = 10;
constant REDIRECT_STATUS_CODES (line 7) | const REDIRECT_STATUS_CODES = new Set([301, 302, 303, 307, 308]);
type SafeFetchOptions (line 9) | interface SafeFetchOptions {
class UrlSafetyError (line 14) | class UrlSafetyError extends Error {
method constructor (line 15) | constructor(message: string) {
type SafeFetch (line 21) | type SafeFetch = (input: string | Request | URL, init?: RequestInit) => ...
FILE: packages/calendar/tests/core/sync-engine/index.test.ts
type GenerationStore (line 398) | type GenerationStore = Parameters<typeof createRedisGenerationCheck>[0];
type GenerationStore (line 412) | type GenerationStore = Parameters<typeof createRedisGenerationCheck>[0];
type GenerationStore (line 427) | type GenerationStore = Parameters<typeof createRedisGenerationCheck>[0];
FILE: packages/calendar/tests/core/sync-engine/ingest.test.ts
type ExistingEvent (line 11) | interface ExistingEvent {
FILE: packages/calendar/tests/core/sync/aggregate-runtime.test.ts
method get (line 47) | get(target, property, receiver) {
FILE: packages/calendar/tests/ics/utils/outlook-windows-timezone.test.ts
constant OUTLOOK_ICS_WITH_WINDOWS_TIMEZONES (line 5) | const OUTLOOK_ICS_WITH_WINDOWS_TIMEZONES = [
FILE: packages/calendar/tests/providers/google/source/provider.test.ts
class TestableGoogleCalendarSourceProvider (line 24) | class TestableGoogleCalendarSourceProvider extends GoogleCalendarSourceP...
method runProcessEvents (line 25) | runProcessEvents(events: SourceEvent[], options: ProcessEventsOptions)...
FILE: packages/calendar/tests/providers/outlook/source/provider.test.ts
class TestableOutlookSourceProvider (line 7) | class TestableOutlookSourceProvider extends OutlookSourceProvider {
method runProcessEvents (line 8) | runProcessEvents(events: SourceEvent[], options: ProcessEventsOptions)...
FILE: packages/calendar/tests/utils/safe-fetch.test.ts
type FetchFn (line 176) | type FetchFn = (input: string | Request | URL, init?: RequestInit) => Pr...
FILE: packages/constants/src/constants/http.ts
constant HTTP_STATUS (line 1) | const HTTP_STATUS = {
type HttpStatus (line 19) | type HttpStatus = (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS];
FILE: packages/constants/src/constants/scopes.ts
constant KEEPER_API_READ_SCOPE (line 1) | const KEEPER_API_READ_SCOPE = "keeper.read";
constant KEEPER_API_SOURCE_SCOPE (line 2) | const KEEPER_API_SOURCE_SCOPE = "keeper.sources.read";
constant KEEPER_API_DESTINATION_SCOPE (line 3) | const KEEPER_API_DESTINATION_SCOPE = "keeper.destinations.read";
constant KEEPER_API_MAPPING_SCOPE (line 4) | const KEEPER_API_MAPPING_SCOPE = "keeper.mappings.read";
constant KEEPER_API_EVENT_SCOPE (line 5) | const KEEPER_API_EVENT_SCOPE = "keeper.events.read";
constant KEEPER_API_SYNC_SCOPE (line 6) | const KEEPER_API_SYNC_SCOPE = "keeper.sync-status.read";
constant KEEPER_API_SCOPES (line 8) | const KEEPER_API_SCOPES = [
constant KEEPER_API_RESOURCE_SCOPES (line 17) | const KEEPER_API_RESOURCE_SCOPES = [...KEEPER_API_SCOPES];
constant KEEPER_API_DEFAULT_SCOPE (line 19) | const KEEPER_API_DEFAULT_SCOPE = ["offline_access", ...KEEPER_API_RESOUR...
FILE: packages/constants/src/constants/time.ts
constant MS_PER_SECOND (line 1) | const MS_PER_SECOND = 1000;
constant SECONDS_PER_MINUTE (line 2) | const SECONDS_PER_MINUTE = 60;
constant MINUTES_PER_HOUR (line 3) | const MINUTES_PER_HOUR = 60;
constant HOURS_PER_DAY (line 4) | const HOURS_PER_DAY = 24;
constant DAYS_PER_WEEK (line 5) | const DAYS_PER_WEEK = 7;
constant REFRESH_BUFFER_MINUTES (line 6) | const REFRESH_BUFFER_MINUTES = 5;
constant RATE_LIMIT_SECONDS (line 7) | const RATE_LIMIT_SECONDS = 60;
constant MS_PER_MINUTE (line 9) | const MS_PER_MINUTE = SECONDS_PER_MINUTE * MS_PER_SECOND;
constant MS_PER_HOUR (line 10) | const MS_PER_HOUR = MINUTES_PER_HOUR * MS_PER_MINUTE;
constant MS_PER_DAY (line 11) | const MS_PER_DAY = HOURS_PER_DAY * MS_PER_HOUR;
constant MS_PER_WEEK (line 12) | const MS_PER_WEEK = DAYS_PER_WEEK * MS_PER_DAY;
constant SECONDS_PER_HOUR (line 14) | const SECONDS_PER_HOUR = MINUTES_PER_HOUR * SECONDS_PER_MINUTE;
constant SECONDS_PER_DAY (line 15) | const SECONDS_PER_DAY = HOURS_PER_DAY * SECONDS_PER_HOUR;
constant TOOLTIP_CLEAR_DELAY_MS (line 17) | const TOOLTIP_CLEAR_DELAY_MS = 1500;
constant TOAST_TIMEOUT_MS (line 18) | const TOAST_TIMEOUT_MS = 3000;
constant WEBSOCKET_RECONNECT_DELAY_MS (line 19) | const WEBSOCKET_RECONNECT_DELAY_MS = 3000;
constant TOKEN_TTL_MS (line 20) | const TOKEN_TTL_MS = 30_000;
constant TOKEN_REFRESH_BUFFER_MS (line 21) | const TOKEN_REFRESH_BUFFER_MS = REFRESH_BUFFER_MINUTES * MS_PER_MINUTE;
constant RATE_LIMIT_DELAY_MS (line 22) | const RATE_LIMIT_DELAY_MS = RATE_LIMIT_SECONDS * MS_PER_SECOND;
constant SYNC_TTL_SECONDS (line 23) | const SYNC_TTL_SECONDS = SECONDS_PER_DAY;
constant KEEPER_EVENT_SUFFIX (line 25) | const KEEPER_EVENT_SUFFIX = "@keeper.sh";
constant KEEPER_USER_EVENT_SUFFIX (line 26) | const KEEPER_USER_EVENT_SUFFIX = "@user.keeper.sh";
constant KEEPER_CATEGORY (line 27) | const KEEPER_CATEGORY = "keeper.sh";
FILE: packages/data-schemas/src/client.ts
type SocketMessage (line 1) | interface SocketMessage {
type SyncAggregate (line 6) | interface SyncAggregate {
FILE: packages/data-schemas/src/index.ts
type ProxyableMethods (line 5) | type ProxyableMethods = typeof proxyableMethods.infer;
type Plan (line 8) | type Plan = typeof planSchema.infer;
type BillingPeriod (line 11) | type BillingPeriod = typeof billingPeriodSchema.infer;
type FeedbackRequest (line 19) | type FeedbackRequest = typeof feedbackRequestSchema.infer;
type CreateSource (line 27) | type CreateSource = typeof createSourceSchema.infer;
type GoogleEvent (line 51) | type GoogleEvent = typeof googleEventSchema.infer;
type GoogleEventList (line 58) | type GoogleEventList = typeof googleEventListSchema.infer;
type GoogleAttendee (line 65) | type GoogleAttendee = typeof googleAttendeeSchema.infer;
type GoogleEventWithAttendees (line 71) | type GoogleEventWithAttendees = typeof googleEventWithAttendeesSchema.in...
type GoogleEventWithAttendeesList (line 77) | type GoogleEventWithAttendeesList = typeof googleEventWithAttendeesListS...
type GoogleApiError (line 88) | type GoogleApiError = typeof googleApiErrorSchema.infer;
type GoogleTokenResponse (line 97) | type GoogleTokenResponse = typeof googleTokenResponseSchema.infer;
type GoogleUserInfo (line 108) | type GoogleUserInfo = typeof googleUserInfoSchema.infer;
type MicrosoftTokenResponse (line 117) | type MicrosoftTokenResponse = typeof microsoftTokenResponseSchema.infer;
type MicrosoftUserInfo (line 125) | type MicrosoftUserInfo = typeof microsoftUserInfoSchema.infer;
type OutlookEvent (line 140) | type OutlookEvent = typeof outlookEventSchema.infer;
type OutlookEventList (line 147) | type OutlookEventList = typeof outlookEventListSchema.infer;
type OutlookCalendarViewEvent (line 161) | type OutlookCalendarViewEvent = typeof outlookCalendarViewEventSchema.in...
type OutlookCalendarViewList (line 167) | type OutlookCalendarViewList = typeof outlookCalendarViewListSchema.infer;
type MicrosoftApiError (line 172) | type MicrosoftApiError = typeof microsoftApiErrorSchema.infer;
type AuthSocialProviders (line 179) | type AuthSocialProviders = typeof authSocialProvidersSchema.infer;
type AuthCapabilities (line 191) | type AuthCapabilities = typeof authCapabilitiesSchema.infer;
type SocketMessage (line 197) | type SocketMessage = typeof socketMessageSchema.infer;
type SyncOperation (line 203) | type SyncOperation = typeof syncOperationSchema.infer;
type SyncStatus (line 218) | type SyncStatus = typeof syncStatusSchema.infer;
type SyncAggregate (line 229) | type SyncAggregate = typeof syncAggregateSchema.infer;
type BroadcastMessage (line 236) | type BroadcastMessage = typeof broadcastMessageSchema.infer;
type User (line 245) | type User = typeof userSchema.infer;
type SignUpBody (line 253) | type SignUpBody = typeof signUpBodySchema.infer;
type CalDAVDiscoverRequest (line 261) | type CalDAVDiscoverRequest = typeof caldavDiscoverRequestSchema.infer;
type CalDAVConnectRequest (line 271) | type CalDAVConnectRequest = typeof caldavConnectRequestSchema.infer;
type UpdateSourceDestinations (line 277) | type UpdateSourceDestinations = typeof updateSourceDestinationsSchema.in...
type CheckoutSuccessEvent (line 284) | type CheckoutSuccessEvent = typeof checkoutSuccessEventSchema.infer;
type GoogleCalendarListEntry (line 295) | type GoogleCalendarListEntry = typeof googleCalendarListEntrySchema.infer;
type GoogleCalendarListResponse (line 302) | type GoogleCalendarListResponse = typeof googleCalendarListResponseSchem...
type CreateOAuthSource (line 313) | type CreateOAuthSource = typeof createOAuthSourceSchema.infer;
type CreateCalDAVSource (line 325) | type CreateCalDAVSource = typeof createCalDAVSourceSchema.infer;
type CalDAVDiscoverSource (line 333) | type CalDAVDiscoverSource = typeof caldavDiscoverSourceSchema.infer;
type OAuthCalendarSource (line 344) | type OAuthCalendarSource = typeof oauthCalendarSourceSchema.infer;
type UpdateOAuthSourceDestinations (line 350) | type UpdateOAuthSourceDestinations = typeof updateOAuthSourceDestination...
FILE: packages/database/drizzle/0000_slimy_justice.sql
type "calendar_snapshots" (line 1) | CREATE TABLE "calendar_snapshots" (
FILE: packages/database/drizzle/0001_complete_golden_guardian.sql
type "remote_ical_sources" (line 1) | CREATE TABLE "remote_ical_sources" (
FILE: packages/database/drizzle/0002_striped_queen_noir.sql
type "calendars" (line 1) | CREATE TABLE "calendars" (
type "event_states" (line 9) | CREATE TABLE "event_states" (
type "users" (line 17) | CREATE TABLE "users" (
FILE: packages/database/drizzle/0005_dusty_nomad.sql
type "account" (line 1) | CREATE TABLE "account" (
type "session" (line 17) | CREATE TABLE "session" (
type "user" (line 29) | CREATE TABLE "user" (
type "verification" (line 40) | CREATE TABLE "verification" (
FILE: packages/database/drizzle/0012_vengeful_thena.sql
type "user_subscriptions" (line 1) | CREATE TABLE "user_subscriptions" (
FILE: packages/database/drizzle/0013_parallel_union_jack.sql
type "sync_status" (line 1) | CREATE TABLE "sync_status" (
type "sync_status" (line 12) | CREATE UNIQUE INDEX "sync_status_user_provider_idx" ON "sync_status" USI...
type "event_states" (line 13) | CREATE INDEX "event_states_start_time_idx" ON "event_states" USING btree...
FILE: packages/database/drizzle/0016_salty_nextwave.sql
type "passkey" (line 1) | CREATE TABLE "passkey" (
FILE: packages/database/drizzle/0019_tearful_doctor_doom.sql
type "calendar_destinations" (line 1) | CREATE TABLE "calendar_destinations" (
type "calendar_destinations" (line 15) | CREATE UNIQUE INDEX "calendar_destinations_user_provider_idx" ON "calend...
FILE: packages/database/drizzle/0020_huge_talon.sql
type "calendar_destinations" (line 2) | CREATE UNIQUE INDEX "calendar_destinations_user_provider_account_idx" ON...
FILE: packages/database/drizzle/0021_icy_white_queen.sql
type "sync_status" (line 6) | CREATE UNIQUE INDEX "sync_status_destination_idx" ON "sync_status" USING...
FILE: packages/database/drizzle/0022_lazy_avengers.sql
type "calendar_destinations" (line 6) | CREATE UNIQUE INDEX "calendar_destinations_provider_account_idx" ON "cal...
FILE: packages/database/drizzle/0023_lyrical_genesis.sql
type "caldav_credentials" (line 1) | CREATE TABLE "caldav_credentials" (
type "oauth_credentials" (line 11) | CREATE TABLE "oauth_credentials" (
FILE: packages/database/drizzle/0024_aberrant_wallop.sql
type "event_mappings" (line 1) | CREATE TABLE "event_mappings" (
type "event_mappings" (line 12) | CREATE UNIQUE INDEX "event_mappings_event_dest_idx" ON "event_mappings" ...
type "event_mappings" (line 13) | CREATE INDEX "event_mappings_destination_idx" ON "event_mappings" USING ...
type "event_states" (line 14) | CREATE UNIQUE INDEX "event_states_identity_idx" ON "event_states" USING ...
FILE: packages/database/drizzle/0028_lush_sumo.sql
type "source_destination_mappings" (line 1) | CREATE TABLE "source_destination_mappings" (
type "source_destination_mappings" (line 10) | CREATE UNIQUE INDEX "source_destination_mapping_idx" ON "source_destinat...
type "source_destination_mappings" (line 11) | CREATE INDEX "source_destination_mappings_source_idx" ON "source_destina...
type "source_destination_mappings" (line 12) | CREATE INDEX "source_destination_mappings_destination_idx" ON "source_de...
FILE: packages/database/drizzle/0030_youthful_speed.sql
type "oauth_calendar_sources" (line 1) | CREATE TABLE "oauth_calendar_sources" (
type "oauth_event_mappings" (line 13) | CREATE TABLE "oauth_event_mappings" (
type "oauth_event_states" (line 24) | CREATE TABLE "oauth_event_states" (
type "oauth_source_destination_mappings" (line 33) | CREATE TABLE "oauth_source_destination_mappings" (
type "oauth_calendar_sources" (line 47) | CREATE UNIQUE INDEX "oauth_calendar_sources_user_calendar_idx" ON "oauth...
type "oauth_calendar_sources" (line 48) | CREATE INDEX "oauth_calendar_sources_user_idx" ON "oauth_calendar_source...
type "oauth_calendar_sources" (line 49) | CREATE INDEX "oauth_calendar_sources_destination_idx" ON "oauth_calendar...
type "oauth_calendar_sources" (line 50) | CREATE INDEX "oauth_calendar_sources_provider_idx" ON "oauth_calendar_so...
type "oauth_event_mappings" (line 51) | CREATE UNIQUE INDEX "oauth_event_mappings_event_dest_idx" ON "oauth_even...
type "oauth_event_mappings" (line 52) | CREATE INDEX "oauth_event_mappings_destination_idx" ON "oauth_event_mapp...
type "oauth_event_states" (line 53) | CREATE INDEX "oauth_event_states_start_time_idx" ON "oauth_event_states"...
type "oauth_event_states" (line 54) | CREATE UNIQUE INDEX "oauth_event_states_identity_idx" ON "oauth_event_st...
type "oauth_event_states" (line 55) | CREATE INDEX "oauth_event_states_source_idx" ON "oauth_event_states" USI...
type "oauth_source_destination_mappings" (line 56) | CREATE UNIQUE INDEX "oauth_source_destination_mapping_idx" ON "oauth_sou...
type "oauth_source_destination_mappings" (line 57) | CREATE INDEX "oauth_source_destination_mappings_source_idx" ON "oauth_so...
type "oauth_source_destination_mappings" (line 58) | CREATE INDEX "oauth_source_destination_mappings_destination_idx" ON "oau...
FILE: packages/database/drizzle/0031_glorious_joshua_kane.sql
type "caldav_event_mappings" (line 1) | CREATE TABLE "caldav_event_mappings" (
type "caldav_event_states" (line 12) | CREATE TABLE "caldav_event_states" (
type "caldav_source_credentials" (line 21) | CREATE TABLE "caldav_source_credentials" (
type "caldav_source_destination_mappings" (line 30) | CREATE TABLE "caldav_source_destination_mappings" (
type "caldav_sources" (line 37) | CREATE TABLE "caldav_sources" (
type "oauth_source_credentials" (line 49) | CREATE TABLE "oauth_source_credentials" (
type "caldav_event_mappings" (line 73) | CREATE UNIQUE INDEX "caldav_event_mappings_event_dest_idx" ON "caldav_ev...
type "caldav_event_mappings" (line 74) | CREATE INDEX "caldav_event_mappings_destination_idx" ON "caldav_event_ma...
type "caldav_event_states" (line 75) | CREATE INDEX "caldav_event_states_start_time_idx" ON "caldav_event_state...
type "caldav_event_states" (line 76) | CREATE UNIQUE INDEX "caldav_event_states_identity_idx" ON "caldav_event_...
type "caldav_event_states" (line 77) | CREATE INDEX "caldav_event_states_source_idx" ON "caldav_event_states" U...
type "caldav_source_destination_mappings" (line 78) | CREATE UNIQUE INDEX "caldav_source_destination_mapping_idx" ON "caldav_s...
type "caldav_source_destination_mappings" (line 79) | CREATE INDEX "caldav_source_destination_mappings_source_idx" ON "caldav_...
type "caldav_source_destination_mappings" (line 80) | CREATE INDEX "caldav_source_destination_mappings_destination_idx" ON "ca...
type "caldav_sources" (line 81) | CREATE INDEX "caldav_sources_user_idx" ON "caldav_sources" USING btree (...
type "caldav_sources" (line 82) | CREATE INDEX "caldav_sources_provider_idx" ON "caldav_sources" USING btr...
type "caldav_sources" (line 83) | CREATE UNIQUE INDEX "caldav_sources_user_calendar_idx" ON "caldav_source...
type "oauth_source_credentials" (line 84) | CREATE INDEX "oauth_source_credentials_user_idx" ON "oauth_source_creden...
type "oauth_source_credentials" (line 85) | CREATE INDEX "oauth_source_credentials_provider_idx" ON "oauth_source_cr...
type "oauth_calendar_sources" (line 87) | CREATE INDEX "oauth_calendar_sources_credential_idx" ON "oauth_calendar_...
type "oauth_calendar_sources" (line 88) | CREATE UNIQUE INDEX "oauth_calendar_sources_user_calendar_idx" ON "oauth...
FILE: packages/database/drizzle/0032_dapper_patch.sql
type "calendar_sources" (line 1) | CREATE TABLE "calendar_sources" (
type "calendar_sources" (line 20) | CREATE INDEX "calendar_sources_user_idx" ON "calendar_sources" USING btr...
type "calendar_sources" (line 21) | CREATE INDEX "calendar_sources_type_idx" ON "calendar_sources" USING btr...
type "calendar_sources" (line 22) | CREATE INDEX "calendar_sources_provider_idx" ON "calendar_sources" USING...
type "event_states" (line 41) | CREATE INDEX "event_states_source_idx" ON "event_states" USING btree ("s...
FILE: packages/database/drizzle/0037_thankful_machine_man.sql
type "calendar_accounts" (line 2) | CREATE TABLE "calendar_accounts" (
type "calendars" (line 16) | CREATE TABLE "calendars" (
type "tmp_source_calendar_id_map" (line 185) | CREATE TEMP TABLE "tmp_source_calendar_id_map" AS
type "tmp_calendar_account_id_map" (line 273) | CREATE TEMP TABLE "tmp_calendar_account_id_map" AS
type "calendar_accounts" (line 394) | CREATE INDEX "calendar_accounts_user_idx" ON "calendar_accounts" USING b...
type "calendar_accounts" (line 395) | CREATE INDEX "calendar_accounts_provider_idx" ON "calendar_accounts" USI...
type "calendars" (line 396) | CREATE INDEX "calendars_user_idx" ON "calendars" USING btree ("userId")
type "calendars" (line 397) | CREATE INDEX "calendars_account_idx" ON "calendars" USING btree ("accoun...
type "calendars" (line 398) | CREATE INDEX "calendars_role_idx" ON "calendars" USING btree ("role")
type "calendars" (line 399) | CREATE INDEX "calendars_type_idx" ON "calendars" USING btree ("calendarT...
type "event_mappings" (line 400) | CREATE UNIQUE INDEX "event_mappings_event_cal_idx" ON "event_mappings" U...
type "event_mappings" (line 401) | CREATE INDEX "event_mappings_calendar_idx" ON "event_mappings" USING btr...
type "event_states" (line 402) | CREATE INDEX "event_states_calendar_idx" ON "event_states" USING btree (...
type "oauth_credentials" (line 403) | CREATE INDEX "oauth_credentials_user_idx" ON "oauth_credentials" USING b...
type "oauth_credentials" (line 404) | CREATE INDEX "oauth_credentials_provider_idx" ON "oauth_credentials" USI...
type "sync_status" (line 405) | CREATE UNIQUE INDEX "sync_status_calendar_idx" ON "sync_status" USING bt...
type "event_states" (line 406) | CREATE UNIQUE INDEX "event_states_identity_idx" ON "event_states" USING ...
type "source_destination_mappings" (line 407) | CREATE UNIQUE INDEX "source_destination_mapping_idx" ON "source_destinat...
type "source_destination_mappings" (line 408) | CREATE INDEX "source_destination_mappings_source_idx" ON "source_destina...
type "source_destination_mappings" (line 409) | CREATE INDEX "source_destination_mappings_destination_idx" ON "source_de...
FILE: packages/database/drizzle/0038_military_radioactive_man.sql
type "calendars" (line 5) | CREATE INDEX "calendars_capabilities_idx" ON "calendars" USING btree ("c...
FILE: packages/database/drizzle/0039_fat_mad_thinker.sql
type "sync_profiles" (line 1) | CREATE TABLE "sync_profiles" (
type "sync_profiles" (line 29) | CREATE INDEX "sync_profiles_user_idx" ON "sync_profiles" USING btree ("u...
type "source_destination_mappings" (line 31) | CREATE INDEX "source_destination_mappings_profile_idx" ON "source_destin...
type "source_destination_mappings" (line 32) | CREATE UNIQUE INDEX "source_destination_mapping_idx" ON "source_destinat...
FILE: packages/database/drizzle/0040_sparkling_toad.sql
type "tmp_calendar_account_id_map_0040" (line 68) | CREATE TEMP TABLE "tmp_calendar_account_id_map_0040" AS
FILE: packages/database/drizzle/0041_keen_black_panther.sql
type "profile_calendars" (line 1) | CREATE TABLE "profile_calendars" (
type "profile_calendars" (line 10) | CREATE UNIQUE INDEX "profile_calendars_unique_idx" ON "profile_calendars...
type "profile_calendars" (line 11) | CREATE INDEX "profile_calendars_profile_idx" ON "profile_calendars" USIN...
FILE: packages/database/drizzle/0042_famous_obadiah_stane.sql
type "source_destination_mappings" (line 12) | CREATE UNIQUE INDEX "source_destination_mapping_idx" ON "source_destinat...
FILE: packages/database/drizzle/0048_gigantic_kid_colt.sql
type "ical_feed_settings" (line 1) | CREATE TABLE "ical_feed_settings" (
FILE: packages/database/drizzle/0050_purple_patch.sql
type "feedback" (line 1) | CREATE TABLE "feedback" (
type "feedback" (line 11) | CREATE INDEX "feedback_user_idx" ON "feedback" USING btree ("userId")
FILE: packages/database/drizzle/0058_same_robbie_robertson.sql
type "oauth_access_token" (line 1) | CREATE TABLE "oauth_access_token" (
type "oauth_application" (line 15) | CREATE TABLE "oauth_application" (
type "oauth_consent" (line 50) | CREATE TABLE "oauth_consent" (
type "oauth_refresh_token" (line 60) | CREATE TABLE "oauth_refresh_token" (
type "oauth_access_token" (line 85) | CREATE INDEX "oauth_access_token_client_idx" ON "oauth_access_token" USI...
type "oauth_access_token" (line 86) | CREATE INDEX "oauth_access_token_session_idx" ON "oauth_access_token" US...
type "oauth_access_token" (line 87) | CREATE INDEX "oauth_access_token_user_idx" ON "oauth_access_token" USING...
type "oauth_access_token" (line 88) | CREATE INDEX "oauth_access_token_refresh_idx" ON "oauth_access_token" US...
type "oauth_application" (line 89) | CREATE INDEX "oauth_application_user_idx" ON "oauth_application" USING b...
type "oauth_consent" (line 90) | CREATE INDEX "oauth_consent_client_idx" ON "oauth_consent" USING btree (...
type "oauth_consent" (line 91) | CREATE INDEX "oauth_consent_user_idx" ON "oauth_consent" USING btree ("u...
type "oauth_consent" (line 92) | CREATE INDEX "oauth_consent_reference_idx" ON "oauth_consent" USING btre...
type "oauth_refresh_token" (line 93) | CREATE INDEX "oauth_refresh_token_client_idx" ON "oauth_refresh_token" U...
type "oauth_refresh_token" (line 94) | CREATE INDEX "oauth_refresh_token_session_idx" ON "oauth_refresh_token" ...
type "oauth_refresh_token" (line 95) | CREATE INDEX "oauth_refresh_token_user_idx" ON "oauth_refresh_token" USI...
FILE: packages/database/drizzle/0059_shocking_stone_men.sql
type "jwks" (line 1) | CREATE TABLE "jwks" (
FILE: packages/database/drizzle/0061_brief_toxin.sql
type "calendar_accounts" (line 1) | CREATE INDEX "calendar_accounts_needs_reauth_idx" ON "calendar_accounts"...
type "event_mappings" (line 2) | CREATE INDEX "event_mappings_sync_hash_idx" ON "event_mappings" USING bt...
type "event_states" (line 3) | CREATE INDEX "event_states_end_time_idx" ON "event_states" USING btree (...
type "oauth_credentials" (line 4) | CREATE INDEX "oauth_credentials_expires_at_idx" ON "oauth_credentials" U...
FILE: packages/database/drizzle/0062_lame_white_tiger.sql
type "api_tokens" (line 1) | CREATE TABLE "api_tokens" (
type "api_tokens" (line 14) | CREATE INDEX "api_tokens_user_idx" ON "api_tokens" USING btree ("userId")
type "api_tokens" (line 15) | CREATE UNIQUE INDEX "api_tokens_hash_idx" ON "api_tokens" USING btree ("...
FILE: packages/database/drizzle/0063_friendly_black_panther.sql
type "user_events" (line 1) | CREATE TABLE "user_events" (
type "user_events" (line 20) | CREATE INDEX "user_events_user_idx" ON "user_events" USING btree ("userId")
type "user_events" (line 21) | CREATE INDEX "user_events_calendar_idx" ON "user_events" USING btree ("c...
type "user_events" (line 22) | CREATE INDEX "user_events_start_time_idx" ON "user_events" USING btree (...
type "user_events" (line 23) | CREATE INDEX "user_events_end_time_idx" ON "user_events" USING btree ("e...
FILE: packages/database/src/database/schema.ts
constant DEFAULT_EVENT_COUNT (line 13) | const DEFAULT_EVENT_COUNT = 0;
FILE: packages/database/src/utils/database.ts
type DatabasePoolOptions (line 5) | interface DatabasePoolOptions {
constant DEFAULT_STATEMENT_TIMEOUT_MS (line 9) | const DEFAULT_STATEMENT_TIMEOUT_MS = 30_000;
type DatabaseInstance (line 17) | interface DatabaseInstance extends BunSQLDatabase {
constant CONNECTION_RETRY_DELAY_MS (line 21) | const CONNECTION_RETRY_DELAY_MS = 500;
constant CONNECTION_MAX_RETRIES (line 22) | const CONNECTION_MAX_RETRIES = 10;
FILE: packages/digest-fetch/src/index.ts
type DigestChallenge (line 66) | interface DigestChallenge {
type FetchFunction (line 142) | type FetchFunction = (
type DigestClientOptions (line 147) | interface DigestClientOptions {
FILE: packages/fixtures/src/cache.ts
constant DEFAULT_FIXTURE_DIRECTORY_SEGMENT (line 3) | const DEFAULT_FIXTURE_DIRECTORY_SEGMENT = "ics";
type FixtureDirectoryOptions (line 5) | interface FixtureDirectoryOptions {
type SyncFixtureFilesOptions (line 9) | interface SyncFixtureFilesOptions extends FixtureDirectoryOptions {
type SyncedFixture (line 15) | interface SyncedFixture {
type MissingFixture (line 21) | interface MissingFixture {
FILE: packages/fixtures/src/schema.ts
type FixtureExpectation (line 9) | type FixtureExpectation = typeof fixtureExpectationSchema.infer;
type FixtureSource (line 21) | type FixtureSource = typeof fixtureSourceSchema.infer;
type FixtureManifest (line 24) | type FixtureManifest = typeof fixtureManifestSchema.infer;
FILE: packages/fixtures/src/scripts/sync-fixtures.ts
constant FORCE_REFRESH_FLAG (line 4) | const FORCE_REFRESH_FLAG = "--refresh";
constant INCLUDE_DISABLED_FLAG (line 5) | const INCLUDE_DISABLED_FLAG = "--include-disabled";
FILE: packages/otelemetry/src/index.ts
constant PINO_SEVERITY (line 16) | const PINO_SEVERITY: Record<number, SeverityNumber> = {
constant PINO_SEVERITY_TEXT (line 25) | const PINO_SEVERITY_TEXT: Record<number, string> = {
FILE: packages/premium/src/constants.ts
constant FREE_ACCOUNT_LIMIT (line 1) | const FREE_ACCOUNT_LIMIT = 2;
constant PRO_ACCOUNT_LIMIT (line 2) | const PRO_ACCOUNT_LIMIT = Infinity;
constant FREE_MAPPING_LIMIT (line 4) | const FREE_MAPPING_LIMIT = 3;
constant PRO_MAPPING_LIMIT (line 5) | const PRO_MAPPING_LIMIT = Infinity;
FILE: packages/premium/src/subscription.ts
constant FIRST_RESULT_LIMIT (line 13) | const FIRST_RESULT_LIMIT = 1;
type PremiumConfig (line 15) | interface PremiumConfig {
type UserSubscription (line 20) | interface UserSubscription {
type PremiumService (line 24) | interface PremiumService {
FILE: packages/queue/src/index.ts
constant PUSH_SYNC_QUEUE_NAME (line 5) | const PUSH_SYNC_QUEUE_NAME = "push-sync";
constant USER_TIMEOUT_MS (line 6) | const USER_TIMEOUT_MS = 300_000;
type PushSyncJobPayload (line 8) | interface PushSyncJobPayload {
type PushSyncJobResult (line 14) | interface PushSyncJobResult {
FILE: packages/sync/src/destination-errors.ts
constant BACKOFF_ERROR_PATTERNS (line 7) | const BACKOFF_ERROR_PATTERNS: string[] = [
FILE: packages/sync/src/resolve-provider.ts
constant OAUTH_PROVIDERS (line 22) | const OAUTH_PROVIDERS = new Set(["google", "outlook"]);
constant CALDAV_PROVIDERS (line 23) | const CALDAV_PROVIDERS = new Set(["caldav", "fastmail", "icloud"]);
type OAuthConfig (line 25) | interface OAuthConfig {
type ResolveProviderOptions (line 150) | interface ResolveProviderOptions {
FILE: packages/sync/src/sync-lock.ts
constant LOCK_PREFIX (line 1) | const LOCK_PREFIX = "sync:lock:";
constant SIGNAL_PREFIX (line 2) | const SIGNAL_PREFIX = "sync:signal:";
constant INVALIDATION_PREFIX (line 3) | const INVALIDATION_PREFIX = "sync:invalidated:";
constant LOCK_TTL_SECONDS (line 4) | const LOCK_TTL_SECONDS = 120;
constant INVALIDATION_TTL_SECONDS (line 5) | const INVALIDATION_TTL_SECONDS = 300;
constant POLL_INTERVAL_MS (line 6) | const POLL_INTERVAL_MS = 250;
constant POLL_TIMEOUT_MS (line 7) | const POLL_TIMEOUT_MS = (LOCK_TTL_SECONDS * 1000) + 10_000;
type SyncLockRedis (line 9) | interface SyncLockRedis {
type InvalidationRedis (line 14) | interface InvalidationRedis {
type SyncLockHandle (line 18) | interface SyncLockHandle {
type AcquireSyncLockResult (line 23) | interface AcquireSyncLockResult {
type SyncLockSkippedResult (line 28) | interface SyncLockSkippedResult {
constant ACQUIRE_OR_SIGNAL_SCRIPT (line 45) | const ACQUIRE_OR_SIGNAL_SCRIPT = `
constant RELEASE_SCRIPT (line 68) | const RELEASE_SCRIPT = `
FILE: packages/sync/src/sync-user.ts
constant GOOGLE_REQUESTS_PER_MINUTE (line 21) | const GOOGLE_REQUESTS_PER_MINUTE = 500;
type SyncConfig (line 58) | interface SyncConfig {
type SyncDestinationsResult (line 68) | interface SyncDestinationsResult {
constant EMPTY_RESULT (line 77) | const EMPTY_RESULT: SyncDestinationsResult = {
type CalendarSyncCompletion (line 86) | interface CalendarSyncCompletion {
type SyncCallbacks (line 100) | interface SyncCallbacks {
FILE: services/api/src/context.ts
constant MIN_TRUSTED_ORIGINS_COUNT (line 16) | const MIN_TRUSTED_ORIGINS_COUNT = 0;
method set (line 25) | async set(key, value, ttlSeconds) {
method consume (line 29) | async consume(key) {
method tryAcquire (line 45) | async tryAcquire(key, ttlSeconds) {
method release (line 49) | async release(key) {
FILE: services/api/src/handlers/auth-oauth-resource.ts
type PrepareOAuthTokenRequestInput (line 1) | interface PrepareOAuthTokenRequestInput {
type PreparedOAuthTokenRequest (line 7) | type PreparedOAuthTokenRequest =
constant TOKEN_ENDPOINT_PATH (line 18) | const TOKEN_ENDPOINT_PATH = "/api/auth/oauth2/token";
constant FORM_URLENCODED_MEDIA_TYPE (line 19) | const FORM_URLENCODED_MEDIA_TYPE = "application/x-www-form-urlencoded";
constant TOKEN_GRANT_TYPES (line 20) | const TOKEN_GRANT_TYPES = new Set(["authorization_code", "refresh_token"]);
FILE: services/api/src/handlers/auth.ts
constant COMPANION_COOKIE_NAME (line 7) | const COMPANION_COOKIE_NAME = "keeper.has_session";
constant COMPANION_COOKIE_SET (line 8) | const COMPANION_COOKIE_SET = `${COMPANION_COOKIE_NAME}=1; Path=/; SameSi...
constant COMPANION_COOKIE_CLEAR (line 9) | const COMPANION_COOKIE_CLEAR = `${COMPANION_COOKIE_NAME}=; Path=/; Max-A...
FILE: services/api/src/handlers/websocket-initial-status.ts
type SocketSender (line 1) | interface SocketSender {
type InitialSyncAggregateFallbackPayload (line 5) | interface InitialSyncAggregateFallbackPayload {
type OutgoingSyncAggregatePayload (line 13) | interface OutgoingSyncAggregatePayload {
type SendInitialSyncStatusDependencies (line 23) | interface SendInitialSyncStatusDependencies {
constant INITIAL_COUNT (line 32) | const INITIAL_COUNT = 0;
constant COMPLETE_PERCENT (line 33) | const COMPLETE_PERCENT = 100;
FILE: services/api/src/handlers/websocket-payload.ts
type SyncAggregatePayload (line 1) | interface SyncAggregatePayload {
type SyncAggregateFallbackPayload (line 11) | type SyncAggregateFallbackPayload = Omit<SyncAggregatePayload, "seq" | "...
type ResolveSyncAggregateDependencies (line 15) | interface ResolveSyncAggregateDependencies {
FILE: services/api/src/handlers/websocket.ts
method close (line 72) | close(socket: Socket) {
method open (line 79) | open(socket: Socket): Promise<void> {
FILE: services/api/src/index.ts
constant HTTP_UNAUTHORIZED (line 15) | const HTTP_UNAUTHORIZED = 401;
constant HTTP_NOT_FOUND (line 16) | const HTTP_NOT_FOUND = 404;
constant HTTP_METHOD_NOT_ALLOWED (line 17) | const HTTP_METHOD_NOT_ALLOWED = 405;
constant HTTP_INTERNAL_SERVER_ERROR (line 18) | const HTTP_INTERNAL_SERVER_ERROR = 500;
FILE: services/api/src/middleware/cors.ts
constant CORS_MAX_AGE_SECONDS (line 4) | const CORS_MAX_AGE_SECONDS = 86_400;
constant HTTP_NO_CONTENT (line 5) | const HTTP_NO_CONTENT = 204;
constant HTTP_FORBIDDEN (line 6) | const HTTP_FORBIDDEN = 403;
constant EMPTY_ORIGINS_COUNT (line 7) | const EMPTY_ORIGINS_COUNT = 0;
type FetchHandler (line 9) | type FetchHandler = (request: Request) => MaybePromise<Response | undefi...
FILE: services/api/src/mutations/index.ts
type OAuthTokenRefresher (line 39) | interface OAuthTokenRefresher {
type MutationDependencies (line 43) | interface MutationDependencies {
constant TOKEN_REFRESH_BUFFER_MS (line 50) | const TOKEN_REFRESH_BUFFER_MS = 60_000;
constant CALDAV_PROVIDERS (line 52) | const CALDAV_PROVIDERS = new Set(["caldav", "fastmail", "icloud"]);
type CreateProviderFailure (line 85) | interface CreateProviderFailure {
type CreateProviderSuccess (line 90) | interface CreateProviderSuccess {
type CreateProviderResult (line 95) | type CreateProviderResult = CreateProviderFailure | CreateProviderSuccess;
FILE: services/api/src/mutations/providers/caldav.ts
type CalDAVCredentials (line 11) | interface CalDAVCredentials {
type CalDAVEventResult (line 87) | interface CalDAVEventResult {
constant PARTSTAT_MAP (line 262) | const PARTSTAT_MAP: Record<RsvpStatus, "ACCEPTED" | "DECLINED" | "TENTAT...
FILE: services/api/src/mutations/providers/google.ts
constant GOOGLE_CALENDAR_API (line 6) | const GOOGLE_CALENDAR_API = "https://www.googleapis.com/calendar/v3/";
type GoogleEventResult (line 22) | interface GoogleEventResult {
type PendingGoogleEvent (line 256) | interface PendingGoogleEvent {
FILE: services/api/src/mutations/providers/outlook.ts
constant MICROSOFT_GRAPH_API (line 6) | const MICROSOFT_GRAPH_API = "https://graph.microsoft.com/v1.0";
type OutlookEventResult (line 19) | interface OutlookEventResult {
constant RSVP_ACTION_MAP (line 189) | const RSVP_ACTION_MAP: Record<RsvpStatus, string> = {
FILE: services/api/src/mutations/resolve-credentials.ts
type CredentialRow (line 31) | interface CredentialRow {
type EventSource (line 142) | type EventSource = "user" | "synced";
type ResolvedEventCredentials (line 144) | interface ResolvedEventCredentials {
FILE: services/api/src/provider-display.ts
type AccountDisplayInput (line 3) | interface AccountDisplayInput {
FILE: services/api/src/queries/get-event-count.ts
constant EMPTY_RESULT_COUNT (line 10) | const EMPTY_RESULT_COUNT = 0;
type EventCountOptions (line 12) | interface EventCountOptions {
FILE: services/api/src/queries/get-events-in-range.ts
constant EMPTY_RESULT_COUNT (line 12) | const EMPTY_RESULT_COUNT = 0;
type SourceInfo (line 34) | interface SourceInfo {
FILE: services/api/src/queries/list-mappings.ts
constant EMPTY_RESULT_COUNT (line 8) | const EMPTY_RESULT_COUNT = 0;
FILE: services/api/src/read-models.ts
type KeeperApiOptions (line 19) | interface KeeperApiOptions {
FILE: services/api/src/routes/api/accounts/[id].ts
constant GET (line 10) | const GET = withWideEvent(
constant DELETE (line 48) | const DELETE = withWideEvent(
FILE: services/api/src/routes/api/accounts/index.ts
constant GET (line 7) | const GET = withWideEvent(
FILE: services/api/src/routes/api/cal/[identifier].ts
constant ICS_EXTENSION_LENGTH (line 5) | const ICS_EXTENSION_LENGTH = 4;
constant GET (line 7) | const GET = withWideEvent(async ({ params }) => {
FILE: services/api/src/routes/api/destinations/[id].ts
constant DELETE (line 6) | const DELETE = withWideEvent(
FILE: services/api/src/routes/api/destinations/authorize.ts
constant FIRST_RESULT_LIMIT (line 9) | const FIRST_RESULT_LIMIT = 1;
constant GET (line 35) | const GET = withWideEvent(
FILE: services/api/src/routes/api/destinations/caldav/discover.ts
constant POST (line 7) | const POST = withWideEvent(
FILE: services/api/src/routes/api/destinations/caldav/index.ts
constant POST (line 11) | const POST = withWideEvent(
FILE: services/api/src/routes/api/destinations/callback/[provider].ts
constant GET (line 13) | const GET = withWideEvent(async ({ request, params }) => {
FILE: services/api/src/routes/api/destinations/index.ts
constant GET (line 7) | const GET = withWideEvent(
FILE: services/api/src/routes/api/entitlements.ts
constant GET (line 7) | const GET = withWideEvent(
FILE: services/api/src/routes/api/events/count.ts
constant GET (line 7) | const GET = withWideEvent(
FILE: services/api/src/routes/api/events/index.ts
constant GET (line 8) | const GET = withWideEvent(
FILE: services/api/src/routes/api/feedback/index.ts
constant TEMPLATE_ID (line 9) | const TEMPLATE_ID = {
constant POST (line 14) | const POST = withWideEvent(
FILE: services/api/src/routes/api/health.ts
constant GET (line 3) | const GET = withWideEvent(() =>
FILE: services/api/src/routes/api/ical/settings.ts
constant DEFAULT_SETTINGS (line 11) | const DEFAULT_SETTINGS = {
constant ICAL_BOOLEAN_UPDATE_FIELDS (line 19) | const ICAL_BOOLEAN_UPDATE_FIELDS = [
type PatchIcalSettingsRouteContext (line 43) | interface PatchIcalSettingsRouteContext {
type PatchIcalSettingsDependencies (line 48) | interface PatchIcalSettingsDependencies {
constant GET (line 80) | const GET = withWideEvent(
constant PATCH (line 92) | const PATCH = withWideEvent(
FILE: services/api/src/routes/api/ical/token.ts
constant GET (line 10) | const GET = withWideEvent(
FILE: services/api/src/routes/api/ics/[id]/destinations.ts
constant GET (line 6) | const GET = withWideEvent(
FILE: services/api/src/routes/api/ics/index.ts
constant GET (line 14) | const GET = withWideEvent(
constant POST (line 24) | const POST = withWideEvent(
FILE: services/api/src/routes/api/ics/source-routes.ts
type IcsRouteContext (line 5) | interface IcsRouteContext {
type IcsPostRouteContext (line 9) | interface IcsPostRouteContext extends IcsRouteContext {
type ParsedCreateSourceBody (line 13) | interface ParsedCreateSourceBody {
type InvalidSourceUrlErrorLike (line 18) | interface InvalidSourceUrlErrorLike {
type GetIcsSourcesDependencies (line 23) | interface GetIcsSourcesDependencies {
type PostIcsSourceDependencies (line 27) | interface PostIcsSourceDependencies {
FILE: services/api/src/routes/api/mappings/index.ts
constant GET (line 7) | const GET = withWideEvent(
FILE: services/api/src/routes/api/socket/token.ts
constant GET (line 4) | const GET = withWideEvent(
FILE: services/api/src/routes/api/socket/url.ts
constant GET (line 5) | const GET = withWideEvent(
FILE: services/api/src/routes/api/sources/[id].ts
constant GET (line 14) | const GET = withWideEvent(
constant PATCH (line 68) | const PATCH = withWideEvent(
FILE: services/api/src/routes/api/sources/[id]/destinations.ts
constant GET (line 14) | const GET = withWideEvent(
constant PUT (line 39) | const PUT = withWideEvent(
FILE: services/api/src/routes/api/sources/[id]/mapping-routes.ts
type MappingRouteContext (line 6) | interface MappingRouteContext {
type MappingPutRouteContext (line 11) | interface MappingPutRouteContext extends MappingRouteContext {
type GetSourceDestinationsDependencies (line 15) | interface GetSourceDestinationsDependencies {
type PutSourceDestinationsDependencies (line 20) | interface PutSourceDestinationsDependencies {
type GetSourcesForDestinationDependencies (line 28) | interface GetSourcesForDestinationDependencies {
type PutSourcesForDestinationDependencies (line 33) | interface PutSourcesForDestinationDependencies {
FILE: services/api/src/routes/api/sources/[id]/source-item-routes.ts
constant EVENT_FILTER_FIELDS (line 6) | const EVENT_FILTER_FIELDS = [
constant SOURCE_BOOLEAN_UPDATE_FIELDS (line 15) | const SOURCE_BOOLEAN_UPDATE_FIELDS = [
type SourceRouteContext (line 20) | interface SourceRouteContext {
type PatchSourceRouteContext (line 25) | interface PatchSourceRouteContext extends SourceRouteContext {
type PatchSourceDependencies (line 29) | interface PatchSourceDependencies {
FILE: services/api/src/routes/api/sources/[id]/sources.ts
constant GET (line 14) | const GET = withWideEvent(
constant PUT (line 37) | const PUT = withWideEvent(
FILE: services/api/src/routes/api/sources/authorize.ts
constant FIRST_RESULT_LIMIT (line 9) | const FIRST_RESULT_LIMIT = 1;
constant GET (line 26) | const GET = withWideEvent(
FILE: services/api/src/routes/api/sources/caldav/discover.ts
constant POST (line 8) | const POST = withWideEvent(
FILE: services/api/src/routes/api/sources/caldav/index.ts
constant GET (line 14) | const GET = withWideEvent(
constant POST (line 34) | const POST = withWideEvent(
FILE: services/api/src/routes/api/sources/callback-state.ts
constant GET (line 6) | const GET = withWideEvent(
FILE: services/api/src/routes/api/sources/callback/[provider].ts
constant MS_PER_SECOND (line 15) | const MS_PER_SECOND = 1000;
constant GET (line 17) | const GET = withWideEvent(async ({ request, params }) => {
FILE: services/api/src/routes/api/sources/google/[id]/destinations.ts
constant GET (line 6) | const GET = withWideEvent(
FILE: services/api/src/routes/api/sources/google/calendars.ts
constant GOOGLE_PROVIDER (line 10) | const GOOGLE_PROVIDER = "google";
constant GET (line 12) | const GET = withWideEvent(
FILE: services/api/src/routes/api/sources/google/index.ts
constant GOOGLE_PROVIDER (line 16) | const GOOGLE_PROVIDER = "google";
constant GET (line 18) | const GET = withWideEvent(
constant POST (line 25) | const POST = withWideEvent(
FILE: services/api/src/routes/api/sources/index.ts
constant GET (line 7) | const GET = withWideEvent(
FILE: services/api/src/routes/api/sources/outlook/[id]/destinations.ts
constant GET (line 6) | const GET = withWideEvent(
FILE: services/api/src/routes/api/sources/outlook/calendars.ts
constant OUTLOOK_PROVIDER (line 10) | const OUTLOOK_PROVIDER = "outlook";
constant GET (line 12) | const GET = withWideEvent(
FILE: services/api/src/routes/api/sources/outlook/index.ts
constant OUTLOOK_PROVIDER (line 15) | const OUTLOOK_PROVIDER = "outlook";
constant GET (line 17) | const GET = withWideEvent(
constant POST (line 24) | const POST = withWideEvent(
FILE: services/api/src/routes/api/sync/status.ts
constant GET (line 7) | const GET = withWideEvent(
FILE: services/api/src/routes/api/tokens/[id].ts
constant DELETE (line 8) | const DELETE = withWideEvent(
FILE: services/api/src/routes/api/tokens/index.ts
constant GET (line 14) | const GET = withWideEvent(
constant POST (line 33) | const POST = withWideEvent(
FILE: services/api/src/routes/api/v1/accounts/index.ts
constant GET (line 7) | const GET = withWideEvent(
FILE: services/api/src/routes/api/v1/calendars/[calendarId]/invites.ts
constant GET (line 13) | const GET = withWideEvent(
FILE: services/api/src/routes/api/v1/calendars/index.ts
constant GET (line 15) | const GET = withWideEvent(
FILE: services/api/src/routes/api/v1/events/[id].ts
constant GET (line 14) | const GET = withWideEvent(
constant PATCH (line 30) | const PATCH = withWideEvent(
constant DELETE (line 74) | const DELETE = withWideEvent(
FILE: services/api/src/routes/api/v1/events/index.ts
constant GET (line 39) | const GET = withWideEvent(
constant POST (line 61) | const POST = withWideEvent(
FILE: services/api/src/routes/api/v1/ical/index.ts
constant GET (line 10) | const GET = withWideEvent(
FILE: services/api/src/routes/api/webhook/polar.ts
constant HTTP_OK (line 8) | const HTTP_OK = 200;
FILE: services/api/src/types.ts
type KeeperDatabase (line 3) | type KeeperDatabase = BunSQLDatabase;
type KeeperEventRangeInput (line 5) | interface KeeperEventRangeInput {
type KeeperEventFilters (line 10) | interface KeeperEventFilters {
type KeeperSource (line 16) | interface KeeperSource {
type KeeperDestination (line 33) | interface KeeperDestination {
type KeeperMapping (line 40) | interface KeeperMapping {
type KeeperEvent (line 48) | interface KeeperEvent {
type KeeperSyncStatus (line 61) | interface KeeperSyncStatus {
type EventInput (line 69) | interface EventInput {
type EventUpdateInput (line 80) | interface EventUpdateInput {
type RsvpStatus (line 90) | type RsvpStatus = "accepted" | "declined" | "tentative";
type EventActionResult (line 92) | interface EventActionResult {
type EventCreateResult (line 97) | interface EventCreateResult extends EventActionResult {
type PendingInvite (line 101) | interface PendingInvite {
type ProviderCredentials (line 114) | interface ProviderCredentials {
type KeeperApi (line 135) | interface KeeperApi {
FILE: services/api/src/utils/api-rate-limit.ts
constant FREE_DAILY_LIMIT (line 3) | const FREE_DAILY_LIMIT = 25;
constant SECONDS_PER_DAY (line 4) | const SECONDS_PER_DAY = 86_400;
type RateLimitResult (line 11) | interface RateLimitResult {
FILE: services/api/src/utils/api-tokens.ts
constant TOKEN_PREFIX (line 3) | const TOKEN_PREFIX = "kpr_";
constant TOKEN_BYTES (line 4) | const TOKEN_BYTES = 32;
constant DISPLAY_PREFIX_LENGTH (line 5) | const DISPLAY_PREFIX_LENGTH = 12;
FILE: services/api/src/utils/background-task.ts
type BackgroundJobCallback (line 3) | type BackgroundJobCallback<TResult> = () => Promise<TResult>;
FILE: services/api/src/utils/caldav-sources.ts
constant FIRST_RESULT_LIMIT (line 13) | const FIRST_RESULT_LIMIT = 1;
constant CALDAV_CALENDAR_TYPE (line 14) | const CALDAV_CALENDAR_TYPE = "caldav";
constant USER_ACCOUNT_LOCK_NAMESPACE (line 15) | const USER_ACCOUNT_LOCK_NAMESPACE = 9002;
type CaldavSourceDatabase (line 16) | type CaldavSourceDatabase = Pick<
class CalDAVSourceLimitError (line 21) | class CalDAVSourceLimitError extends Error {
method constructor (line 22) | constructor() {
class DuplicateCalDAVSourceError (line 27) | class DuplicateCalDAVSourceError extends Error {
method constructor (line 28) | constructor() {
type CalDAVSource (line 33) | interface CalDAVSource {
type CreateCalDAVSourceData (line 45) | interface CreateCalDAVSourceData {
FILE: services/api/src/utils/caldav.ts
constant USER_ACCOUNT_LOCK_NAMESPACE (line 12) | const USER_ACCOUNT_LOCK_NAMESPACE = 9002;
class DestinationLimitError (line 14) | class DestinationLimitError extends Error {
method constructor (line 15) | constructor() {
class CalDAVConnectionError (line 20) | class CalDAVConnectionError extends Error {
method constructor (line 21) | constructor(cause?: unknown) {
type CalDAVCredentials (line 27) | interface CalDAVCredentials {
type DiscoveredCalendar (line 32) | interface DiscoveredCalendar {
type DiscoveryResult (line 37) | interface DiscoveryResult {
FILE: services/api/src/utils/date-range.ts
constant HOURS_START_OF_DAY (line 3) | const HOURS_START_OF_DAY = 0;
constant MINUTES_START (line 4) | const MINUTES_START = 0;
constant SECONDS_START (line 5) | const SECONDS_START = 0;
constant MILLISECONDS_START (line 6) | const MILLISECONDS_START = 0;
constant HOURS_END_OF_DAY (line 7) | const HOURS_END_OF_DAY = 23;
constant MINUTES_END (line 8) | const MINUTES_END = 59;
constant SECONDS_END (line 9) | const SECONDS_END = 59;
constant MILLISECONDS_END (line 10) | const MILLISECONDS_END = 999;
type DateRange (line 12) | interface DateRange {
type NormalizedDateRange (line 17) | interface NormalizedDateRange {
FILE: services/api/src/utils/destinations.ts
constant FIRST_RESULT_LIMIT (line 19) | const FIRST_RESULT_LIMIT = 1;
constant EMPTY_RESULT_COUNT (line 20) | const EMPTY_RESULT_COUNT = 0;
type DestinationDatabase (line 21) | type DestinationDatabase = Pick<typeof database, "delete" | "insert" | "...
type CalendarDestination (line 53) | interface CalendarDestination {
type ExistingAccount (line 60) | interface ExistingAccount {
type AccountInsertData (line 114) | interface AccountInsertData {
FILE: services/api/src/utils/enqueue-push-sync.ts
type PushSyncQueue (line 5) | interface PushSyncQueue {
type EnqueuePushSyncDependencies (line 10) | interface EnqueuePushSyncDependencies {
FILE: services/api/src/utils/ical-format.ts
type FeedSettings (line 6) | interface FeedSettings {
type CalendarEvent (line 14) | interface CalendarEvent {
FILE: services/api/src/utils/ical.ts
constant DEFAULT_FEED_SETTINGS (line 8) | const DEFAULT_FEED_SETTINGS: FeedSettings = {
FILE: services/api/src/utils/invalidate-calendars.ts
constant INVALIDATION_PREFIX (line 6) | const INVALIDATION_PREFIX = "sync:invalidated:";
constant INVALIDATION_TTL_SECONDS (line 7) | const INVALIDATION_TTL_SECONDS = 300;
FILE: services/api/src/utils/middleware.ts
constant MS_PER_DAY (line 12) | const MS_PER_DAY = 86_400_000;
type RouteContext (line 14) | interface RouteContext {
type AuthenticatedRouteContext (line 19) | interface AuthenticatedRouteContext extends RouteContext {
type RouteHandler (line 23) | type RouteHandler = (request: Request, params: Record<string, string>, r...
type RouteCallback (line 25) | type RouteCallback = (ctx: RouteContext) => MaybePromise<Response>;
type AuthenticatedRouteCallback (line 26) | type AuthenticatedRouteCallback = (ctx: AuthenticatedRouteContext) => Ma...
type UserContext (line 59) | interface UserContext {
type Session (line 81) | interface Session {
FILE: services/api/src/utils/oauth-calendar-listing.ts
type CredentialsWithExpiry (line 18) | interface CredentialsWithExpiry {
type RefreshTokenResult (line 24) | interface RefreshTokenResult {
type CalendarListAuthError (line 28) | interface CalendarListAuthError extends Error {
type NormalizedCalendar (line 32) | interface NormalizedCalendar {
type OAuthCalendarListingOptions (line 38) | interface OAuthCalendarListingOptions {
type OAuthCalendarAccessOptions (line 52) | type OAuthCalendarAccessOptions = Pick<
FILE: services/api/src/utils/oauth-callback-state.ts
constant CALLBACK_STATE_PREFIX (line 3) | const CALLBACK_STATE_PREFIX = "oauth:callback:";
constant CALLBACK_STATE_TTL_SECONDS (line 4) | const CALLBACK_STATE_TTL_SECONDS = 300;
type OAuthCallbackState (line 6) | interface OAuthCallbackState {
FILE: services/api/src/utils/oauth-refresh.ts
constant FIRST_RESULT_LIMIT (line 10) | const FIRST_RESULT_LIMIT = 1;
constant MS_PER_SECOND (line 11) | const MS_PER_SECOND = 1000;
type RefreshResult (line 13) | interface RefreshResult {
FILE: services/api/src/utils/oauth-source-credentials.ts
constant FIRST_RESULT_LIMIT (line 5) | const FIRST_RESULT_LIMIT = 1;
type CreateOAuthSourceCredentialData (line 7) | interface CreateOAuthSourceCredentialData {
FILE: services/api/src/utils/oauth-sources.ts
constant FIRST_RESULT_LIMIT (line 17) | const FIRST_RESULT_LIMIT = 1;
constant OAUTH_CALENDAR_TYPE (line 18) | const OAUTH_CALENDAR_TYPE = "oauth";
constant USER_ACCOUNT_LOCK_NAMESPACE (line 19) | const USER_ACCOUNT_LOCK_NAMESPACE = 9002;
type OAuthSourceDatabase (line 20) | type OAuthSourceDatabase = Pick<typeof contextDatabase, "insert" | "sele...
class OAuthSourceLimitError (line 22) | class OAuthSourceLimitError extends Error {
method constructor (line 23) | constructor() {
class DestinationNotFoundError (line 28) | class DestinationNotFoundError extends Error {
method constructor (line 29) | constructor() {
class DestinationProviderMismatchError (line 34) | class DestinationProviderMismatchError extends Error {
method constructor (line 35) | constructor(provider: string) {
class DuplicateSourceError (line 40) | class DuplicateSourceError extends Error {
method constructor (line 41) | constructor() {
type OAuthCalendarSource (line 46) | interface OAuthCalendarSource {
type OAuthAccountWithCredentials (line 53) | interface OAuthAccountWithCredentials {
type OAuthSourceWithCredentials (line 61) | interface OAuthSourceWithCredentials {
class SourceCredentialNotFoundError (line 176) | class SourceCredentialNotFoundError extends Error {
method constructor (line 177) | constructor() {
class SourceCredentialProviderMismatchError (line 182) | class SourceCredentialProviderMismatchError extends Error {
method constructor (line 183) | constructor(provider: string) {
type CreateOAuthSourceOptions (line 229) | interface CreateOAuthSourceOptions {
type CreateOAuthSourceDependencies (line 239) | interface CreateOAuthSourceDependencies {
type ImportOAuthAccountDependencies (line 598) | interface ImportOAuthAccountDependencies {
type ExternalCalendar (line 766) | interface ExternalCalendar {
type ImportOAuthAccountOptions (line 771) | interface ImportOAuthAccountOptions {
FILE: services/api/src/utils/oauth.ts
constant MS_PER_SECOND (line 9) | const MS_PER_SECOND = 1000;
constant USER_ACCOUNT_LOCK_NAMESPACE (line 10) | const USER_ACCOUNT_LOCK_NAMESPACE = 9002;
type OAuthCallbackParams (line 12) | interface OAuthCallbackParams {
class OAuthError (line 49) | class OAuthError extends Error {
method constructor (line 52) | constructor(
constant ACCOUNT_LIMIT_ERROR_MESSAGE (line 62) | const ACCOUNT_LIMIT_ERROR_MESSAGE = "Account limit reached. Upgrade to P...
type OAuthUserInfo (line 64) | interface OAuthUserInfo {
type OAuthTokenResponse (line 69) | interface OAuthTokenResponse {
type HandleOAuthCallbackDependencies (line 76) | interface HandleOAuthCallbackDependencies {
FILE: services/api/src/utils/provider-display.ts
type AccountDisplayInput (line 3) | interface AccountDisplayInput {
FILE: services/api/src/utils/request-body.ts
type CalendarIdsBody (line 7) | type CalendarIdsBody = typeof calendarIdsBodySchema.infer;
type SourcePatchBody (line 21) | type SourcePatchBody = typeof sourcePatchBodySchema.infer;
type IcalSettingsPatchBody (line 31) | type IcalSettingsPatchBody = typeof icalSettingsPatchBodySchema.infer;
type EventCreateBody (line 44) | type EventCreateBody = typeof eventCreateBodySchema.infer;
type EventPatchBody (line 57) | type EventPatchBody = typeof eventPatchBodySchema.infer;
type TokenCreateBody (line 63) | type TokenCreateBody = typeof tokenCreateBodySchema.infer;
FILE: services/api/src/utils/request-query.ts
type SourceAuthorizeQuery (line 8) | type SourceAuthorizeQuery = typeof sourceAuthorizeQuerySchema.infer;
type DestinationAuthorizeQuery (line 15) | type DestinationAuthorizeQuery = typeof destinationAuthorizeQuerySchema....
type CallbackStateQuery (line 21) | type CallbackStateQuery = typeof callbackStateQuerySchema.infer;
type SocketQuery (line 27) | type SocketQuery = typeof socketQuerySchema.infer;
type OAuthCallbackQuery (line 34) | type OAuthCallbackQuery = typeof oauthCallbackQuerySchema.infer;
type OAuthCalendarListingQuery (line 48) | type OAuthCalendarListingQuery = typeof oauthCalendarListingQuerySchema....
type CaldavSourcesQuery (line 54) | type CaldavSourcesQuery = typeof caldavSourcesQuerySchema.infer;
type ProviderParam (line 60) | type ProviderParam = typeof providerParamSchema.infer;
type IdParam (line 66) | type IdParam = typeof idParamSchema.infer;
FILE: services/api/src/utils/responses.ts
class ErrorResponse (line 3) | class ErrorResponse {
method constructor (line 7) | constructor(status: number, message: string | null = null) {
method toResponse (line 12) | toResponse(): Response {
method badRequest (line 16) | static badRequest(message: string | null = null): ErrorResponse {
method unauthorized (line 20) | static unauthorized(message: string | null = null): ErrorResponse {
method paymentRequired (line 24) | static paymentRequired(message: string | null = null): ErrorResponse {
method forbidden (line 28) | static forbidden(message: string | null = null): ErrorResponse {
method notFound (line 32) | static notFound(message: string | null = null): ErrorResponse {
method conflict (line 36) | static conflict(message: string | null = null): ErrorResponse {
method tooManyRequests (line 40) | static tooManyRequests(message: string | null = null): ErrorResponse {
method notImplemented (line 44) | static notImplemented(message: string | null = null): ErrorResponse {
method internal (line 48) | static internal(message: string | null = null): ErrorResponse {
FILE: services/api/src/utils/route-handler.ts
type HttpMethod (line 3) | type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
type RouteModule (line 5) | interface RouteModule {
constant HTTP_METHODS (line 14) | const HTTP_METHODS: HttpMethod[] = ["GET", "POST", "PUT", "PATCH", "DELE...
FILE: services/api/src/utils/source-destination-mappings.ts
constant EMPTY_LIST_COUNT (line 8) | const EMPTY_LIST_COUNT = 0;
constant USER_MAPPING_LOCK_NAMESPACE (line 9) | const USER_MAPPING_LOCK_NAMESPACE = 9001;
constant MAPPING_LIMIT_ERROR_MESSAGE (line 10) | const MAPPING_LIMIT_ERROR_MESSAGE = "Mapping limit reached. Upgrade to P...
type DatabaseClient (line 12) | type DatabaseClient = typeof databaseInstance;
type DatabaseTransactionCallback (line 13) | type DatabaseTransactionCallback = Parameters<DatabaseClient["transactio...
type DatabaseTransactionClient (line 14) | type DatabaseTransactionClient = Parameters<DatabaseTransactionCallback>...
type SourceDestinationMapping (line 16) | interface SourceDestinationMapping {
type SetDestinationsTransaction (line 24) | interface SetDestinationsTransaction {
type SetDestinationsDependencies (line 40) | interface SetDestinationsDependencies {
type SetSourcesTransaction (line 47) | interface SetSourcesTransaction {
type SetSourcesDependencies (line 60) | interface SetSourcesDependencies {
FILE: services/api/src/utils/source-lifecycle.ts
type SourceReference (line 3) | interface SourceReference {
type CreateSourceInput (line 7) | interface CreateSourceInput {
type CreateSourceDependencies (line 13) | interface CreateSourceDependencies<TSource extends SourceReference> {
class SourceLimitError (line 37) | class SourceLimitError extends Error {
method constructor (line 38) | constructor() {
class InvalidSourceUrlError (line 43) | class InvalidSourceUrlError extends Error {
method constructor (line 46) | constructor(cause?: unknown) {
FILE: services/api/src/utils/source-sync-defaults.ts
constant DEFAULT_SOURCE_SYNC_RULES (line 1) | const DEFAULT_SOURCE_SYNC_RULES = {
FILE: services/api/src/utils/sources.ts
constant USER_ACCOUNT_LOCK_NAMESPACE (line 18) | const USER_ACCOUNT_LOCK_NAMESPACE = 9002;
constant FIRST_RESULT_LIMIT (line 20) | const FIRST_RESULT_LIMIT = 1;
constant ICAL_CALENDAR_TYPE (line 21) | const ICAL_CALENDAR_TYPE = "ical";
type Source (line 22) | type Source = typeof calendarsTable.$inferSelect;
FILE: services/api/src/utils/state.ts
constant SOCKET_TOKEN_PREFIX (line 4) | const SOCKET_TOKEN_PREFIX = "socket:token:";
constant TOKEN_TTL_SECONDS (line 5) | const TOKEN_TTL_SECONDS = Math.ceil(TOKEN_TTL_MS / 1000);
FILE: services/api/src/utils/user.ts
constant FIRST_RESULT_LIMIT (line 5) | const FIRST_RESULT_LIMIT = 1;
FILE: services/api/tests/routes/api/ics/source-routes.test.ts
class TestSourceLimitError (line 9) | class TestSourceLimitError extends Error {}
class TestInvalidSourceUrlError (line 11) | class TestInvalidSourceUrlError extends Error {
method constructor (line 14) | constructor(message: string, authRequired: boolean) {
FILE: services/api/tests/utils/account-locks.test.ts
type SelectPromise (line 33) | type SelectPromise = Promise<unknown[]> & {
FILE: services/api/tests/utils/enqueue-push-sync.test.ts
type AddedJob (line 6) | interface AddedJob {
method addedJobs (line 25) | get addedJobs() { return state.addedJobs; }
method closed (line 25) | get closed() { return state.closed; }
FILE: services/api/tests/utils/ical.test.ts
type SummaryEvent (line 8) | interface SummaryEvent {
type SummarySettings (line 13) | interface SummarySettings {
constant DEFAULT_SETTINGS (line 30) | const DEFAULT_SETTINGS = {
FILE: services/api/tests/utils/source-destination-mappings.test.ts
type UserLockManager (line 51) | interface UserLockManager {
FILE: services/api/tests/utils/source-lifecycle.test.ts
type TestSource (line 8) | interface TestSource {
FILE: services/cron/scripts/build.ts
constant ROUTES_GLOB (line 3) | const ROUTES_GLOB = new Glob("src/jobs/**/*.ts");
constant ENTRY_POINT_GLOB (line 4) | const ENTRY_POINT_GLOB = new Glob("src/index.ts");
FILE: services/cron/src/context.ts
constant REDIS_COMMAND_TIMEOUT_MS (line 15) | const REDIS_COMMAND_TIMEOUT_MS = 10_000;
method tryAcquire (line 18) | async tryAcquire(key, ttlSeconds) {
method release (line 22) | async release(key) {
FILE: services/cron/src/jobs/ingest-sources.ts
constant SOURCE_TIMEOUT_MS (line 32) | const SOURCE_TIMEOUT_MS = 60_000;
constant SOURCE_CONCURRENCY (line 33) | const SOURCE_CONCURRENCY = 5;
constant GOOGLE_REQUESTS_PER_MINUTE (line 34) | const GOOGLE_REQUESTS_PER_MINUTE = 500;
type OAuthFetcherParams (line 142) | interface OAuthFetcherParams {
type IngestionSourceResult (line 162) | interface IngestionSourceResult {
method callback (line 567) | async callback() {
FILE: services/cron/src/jobs/push-destinations.ts
method callback (line 45) | async callback() {
FILE: services/cron/src/jobs/reconcile-subscriptions.ts
constant EMPTY_SUBSCRIPTIONS_COUNT (line 7) | const EMPTY_SUBSCRIPTIONS_COUNT = 0;
type ReconcileSubscriptionsDependencies (line 16) | interface ReconcileSubscriptionsDependencies {
constant RECONCILE_USER_TIMEOUT_MS (line 23) | const RECONCILE_USER_TIMEOUT_MS = 60_000;
method callback (line 110) | async callback() {
FILE: services/cron/src/migration-check.ts
constant MIGRATION_GUIDE_URL (line 1) | const MIGRATION_GUIDE_URL = "https://github.com/ridafkih/keeper.sh/issue...
FILE: services/cron/src/utils/source-plan-selection.ts
type SourceWithUserId (line 3) | interface SourceWithUserId {
type GetUserPlan (line 7) | type GetUserPlan = (userId: string) => Promise<Plan>;
FILE: services/cron/tests/utils/source-plan-selection.test.ts
type Plan (line 4) | type Plan = "free" | "pro";
type SourceRecord (line 6) | interface SourceRecord {
FILE: services/mcp/src/env.ts
type McpEnv (line 14) | type McpEnv = ReturnType<typeof loadMcpEnv>;
FILE: services/mcp/src/index.ts
constant HTTP_NOT_FOUND (line 13) | const HTTP_NOT_FOUND = 404;
constant HTTP_METHOD_NOT_ALLOWED (line 14) | const HTTP_METHOD_NOT_ALLOWED = 405;
constant HTTP_INTERNAL_SERVER_ERROR (line 15) | const HTTP_INTERNAL_SERVER_ERROR = 500;
FILE: services/mcp/src/mcp-handler.ts
constant JSON_RPC_VERSION (line 7) | const JSON_RPC_VERSION = "2.0";
constant JSON_RPC_ERROR_UNAUTHORIZED (line 8) | const JSON_RPC_ERROR_UNAUTHORIZED = -32_001;
constant JSON_RPC_ERROR_FORBIDDEN (line 9) | const JSON_RPC_ERROR_FORBIDDEN = -32_003;
constant JSON_RPC_ERROR_METHOD_NOT_ALLOWED (line 10) | const JSON_RPC_ERROR_METHOD_NOT_ALLOWED = -32_005;
constant ALLOWED_HTTP_METHODS (line 11) | const ALLOWED_HTTP_METHODS = new Set(["DELETE", "GET", "POST"]);
constant ALLOW_HEADER_VALUE (line 12) | const ALLOW_HEADER_VALUE = "GET, POST, DELETE, OPTIONS";
type ResponseHeaders (line 13) | type ResponseHeaders = Headers | Record<string, string>;
type KeeperMcpAuthSession (line 15) | interface KeeperMcpAuthSession {
type AuthenticatedKeeperMcpSession (line 20) | interface AuthenticatedKeeperMcpSession {
type KeeperMcpAuth (line 26) | interface KeeperMcpAuth {
type McpSessionResolution (line 32) | type McpSessionResolution =
type CreateKeeperMcpHandlerOptions (line 41) | interface CreateKeeperMcpHandlerOptions {
FILE: services/mcp/src/routes/health.ts
constant GET (line 3) | const GET = withWideEvent(() =>
FILE: services/mcp/src/toolset.ts
type KeeperEvent (line 16) | type KeeperEvent = z.infer<typeof keeperEventSchema>;
type KeeperToolContext (line 18) | interface KeeperToolContext {
type KeeperMcpToolDefinition (line 23) | interface KeeperMcpToolDefinition<TResult> {
type KeeperCalendar (line 36) | type KeeperCalendar = z.infer<typeof keeperCalendarSchema>;
type KeeperMcpToolset (line 38) | interface KeeperMcpToolset {
FILE: services/mcp/src/utils/middleware.ts
type RouteHandler (line 3) | type RouteHandler = (request: Request) => Response | Promise<Response>;
FILE: services/mcp/src/utils/route-handler.ts
type RouteHandler (line 1) | type RouteHandler = (request: Request) => Response | Promise<Response>;
type HttpMethod (line 3) | type HttpMethod = "GET" | "POST" | "DELETE";
type RouteModule (line 5) | interface RouteModule {
constant HTTP_METHODS (line 11) | const HTTP_METHODS: HttpMethod[] = ["GET", "POST", "DELETE"];
FILE: services/worker/scripts/build.ts
constant ENTRY_POINT_GLOB (line 3) | const ENTRY_POINT_GLOB = new Glob("src/index.ts");
FILE: services/worker/src/context.ts
constant REDIS_COMMAND_TIMEOUT_MS (line 8) | const REDIS_COMMAND_TIMEOUT_MS = 10_000;
constant REDIS_MAX_RETRIES (line 9) | const REDIS_MAX_RETRIES = 3;
method tryAcquire (line 12) | async tryAcquire(key, ttlSeconds) {
method release (line 16) | async release(key) {
FILE: services/worker/src/index.ts
constant DEFAULT_CONCURRENCY (line 10) | const DEFAULT_CONCURRENCY = 25;
constant LOCK_DURATION_MS (line 11) | const LOCK_DURATION_MS = 360_000;
constant STALLED_INTERVAL_MS (line 12) | const STALLED_INTERVAL_MS = 30_000;
constant MAX_STALLED_COUNT (line 13) | const MAX_STALLED_COUNT = 1;
Copy disabled (too large)
Download .json
Condensed preview — 913 files, each showing path, character count, and a content snippet. Download the .json file for the full structured content (10,014K chars).
[
{
"path": ".claude/settings.json",
"chars": 469,
"preview": "{\n \"$schema\": \"https://json.schemastore.org/claude-code-settings.json\",\n \"hooks\": {\n \"PostToolUse\": [\n {\n "
},
{
"path": ".claude/skills/react-best-practices/AGENTS.md",
"chars": 60499,
"preview": "# React Best Practices\n\n**Version 0.1.0** \nVercel Engineering \nJanuary 2026\n\n> **Note:** \n> This document is mainly f"
},
{
"path": ".claude/skills/react-best-practices/README.md",
"chars": 3360,
"preview": "# React Best Practices\n\nA structured repository for creating and maintaining React Best Practices optimized for agents a"
},
{
"path": ".claude/skills/react-best-practices/SKILL.md",
"chars": 5257,
"preview": "---\nname: vercel-react-best-practices\ndescription: React and Next.js performance optimization guidelines from Vercel Eng"
},
{
"path": ".claude/skills/react-best-practices/metadata.json",
"chars": 921,
"preview": "{\n \"version\": \"0.1.0\",\n \"organization\": \"Vercel Engineering\",\n \"date\": \"January 2026\",\n \"abstract\": \"Comprehensive p"
},
{
"path": ".claude/skills/react-best-practices/rules/_sections.md",
"chars": 1554,
"preview": "# Sections\n\nThis file defines all sections, their ordering, impact levels, and descriptions.\nThe section ID (in parenthe"
},
{
"path": ".claude/skills/react-best-practices/rules/_template.md",
"chars": 631,
"preview": "---\ntitle: Rule Title Here\nimpact: MEDIUM\nimpactDescription: Optional description of impact (e.g., \"20-50% improvement\")"
},
{
"path": ".claude/skills/react-best-practices/rules/advanced-event-handler-refs.md",
"chars": 1478,
"preview": "---\ntitle: Store Event Handlers in Refs\nimpact: LOW\nimpactDescription: stable subscriptions\ntags: advanced, hooks, refs,"
},
{
"path": ".claude/skills/react-best-practices/rules/advanced-use-latest.md",
"chars": 1191,
"preview": "---\ntitle: useLatest for Stable Callback Refs\nimpact: LOW\nimpactDescription: prevents effect re-runs\ntags: advanced, hoo"
},
{
"path": ".claude/skills/react-best-practices/rules/async-api-routes.md",
"chars": 1124,
"preview": "---\ntitle: Prevent Waterfall Chains in API Routes\nimpact: CRITICAL\nimpactDescription: 2-10× improvement\ntags: api-routes"
},
{
"path": ".claude/skills/react-best-practices/rules/async-defer-await.md",
"chars": 2028,
"preview": "---\ntitle: Defer Await Until Needed\nimpact: HIGH\nimpactDescription: avoids blocking unused code paths\ntags: async, await"
},
{
"path": ".claude/skills/react-best-practices/rules/async-dependencies.md",
"chars": 941,
"preview": "---\ntitle: Dependency-Based Parallelization\nimpact: CRITICAL\nimpactDescription: 2-10× improvement\ntags: async, paralleli"
},
{
"path": ".claude/skills/react-best-practices/rules/async-parallel.md",
"chars": 653,
"preview": "---\ntitle: Promise.all() for Independent Operations\nimpact: CRITICAL\nimpactDescription: 2-10× improvement\ntags: async, p"
},
{
"path": ".claude/skills/react-best-practices/rules/async-suspense-boundaries.md",
"chars": 2508,
"preview": "---\ntitle: Strategic Suspense Boundaries\nimpact: HIGH\nimpactDescription: faster initial paint\ntags: async, suspense, str"
},
{
"path": ".claude/skills/react-best-practices/rules/bundle-barrel-imports.md",
"chars": 2370,
"preview": "---\ntitle: Avoid Barrel File Imports\nimpact: CRITICAL\nimpactDescription: 200-800ms import cost, slow builds\ntags: bundle"
},
{
"path": ".claude/skills/react-best-practices/rules/bundle-conditional.md",
"chars": 866,
"preview": "---\ntitle: Conditional Module Loading\nimpact: HIGH\nimpactDescription: loads large data only when needed\ntags: bundle, co"
},
{
"path": ".claude/skills/react-best-practices/rules/bundle-defer-third-party.md",
"chars": 920,
"preview": "---\ntitle: Defer Non-Critical Third-Party Libraries\nimpact: MEDIUM\nimpactDescription: loads after hydration\ntags: bundle"
},
{
"path": ".claude/skills/react-best-practices/rules/bundle-dynamic-imports.md",
"chars": 791,
"preview": "---\ntitle: Dynamic Imports for Heavy Components\nimpact: CRITICAL\nimpactDescription: directly affects TTI and LCP\ntags: b"
},
{
"path": ".claude/skills/react-best-practices/rules/bundle-preload.md",
"chars": 1149,
"preview": "---\ntitle: Preload Based on User Intent\nimpact: MEDIUM\nimpactDescription: reduces perceived latency\ntags: bundle, preloa"
},
{
"path": ".claude/skills/react-best-practices/rules/client-event-listeners.md",
"chars": 1969,
"preview": "---\ntitle: Deduplicate Global Event Listeners\nimpact: LOW\nimpactDescription: single listener for N components\ntags: clie"
},
{
"path": ".claude/skills/react-best-practices/rules/client-swr-dedup.md",
"chars": 1159,
"preview": "---\ntitle: Use SWR for Automatic Deduplication\nimpact: MEDIUM-HIGH\nimpactDescription: automatic deduplication\ntags: clie"
},
{
"path": ".claude/skills/react-best-practices/rules/js-batch-dom-css.md",
"chars": 1902,
"preview": "---\ntitle: Batch DOM CSS Changes\nimpact: MEDIUM\nimpactDescription: reduces reflows/repaints\ntags: javascript, dom, css, "
},
{
"path": ".claude/skills/react-best-practices/rules/js-cache-function-results.md",
"chars": 1949,
"preview": "---\ntitle: Cache Repeated Function Calls\nimpact: MEDIUM\nimpactDescription: avoid redundant computation\ntags: javascript,"
},
{
"path": ".claude/skills/react-best-practices/rules/js-cache-property-access.md",
"chars": 531,
"preview": "---\ntitle: Cache Property Access in Loops\nimpact: LOW-MEDIUM\nimpactDescription: reduces lookups\ntags: javascript, loops,"
},
{
"path": ".claude/skills/react-best-practices/rules/js-cache-storage.md",
"chars": 1651,
"preview": "---\ntitle: Cache Storage API Calls\nimpact: LOW-MEDIUM\nimpactDescription: reduces expensive I/O\ntags: javascript, localSt"
},
{
"path": ".claude/skills/react-best-practices/rules/js-combine-iterations.md",
"chars": 753,
"preview": "---\ntitle: Combine Multiple Array Iterations\nimpact: LOW-MEDIUM\nimpactDescription: reduces iterations\ntags: javascript, "
},
{
"path": ".claude/skills/react-best-practices/rules/js-early-exit.md",
"chars": 1133,
"preview": "---\ntitle: Early Return from Functions\nimpact: LOW-MEDIUM\nimpactDescription: avoids unnecessary computation\ntags: javasc"
},
{
"path": ".claude/skills/react-best-practices/rules/js-hoist-regexp.md",
"chars": 1028,
"preview": "---\ntitle: Hoist RegExp Creation\nimpact: LOW-MEDIUM\nimpactDescription: avoids recreation\ntags: javascript, regexp, optim"
},
{
"path": ".claude/skills/react-best-practices/rules/js-index-maps.md",
"chars": 834,
"preview": "---\ntitle: Build Index Maps for Repeated Lookups\nimpact: LOW-MEDIUM\nimpactDescription: 1M ops to 2K ops\ntags: javascript"
},
{
"path": ".claude/skills/react-best-practices/rules/js-length-check-first.md",
"chars": 1752,
"preview": "---\ntitle: Early Length Check for Array Comparisons\nimpact: MEDIUM-HIGH\nimpactDescription: avoids expensive operations w"
},
{
"path": ".claude/skills/react-best-practices/rules/js-min-max-loop.md",
"chars": 2105,
"preview": "---\ntitle: Use Loop for Min/Max Instead of Sort\nimpact: LOW\nimpactDescription: O(n) instead of O(n log n)\ntags: javascri"
},
{
"path": ".claude/skills/react-best-practices/rules/js-set-map-lookups.md",
"chars": 532,
"preview": "---\ntitle: Use Set/Map for O(1) Lookups\nimpact: LOW-MEDIUM\nimpactDescription: O(n) to O(1)\ntags: javascript, set, map, d"
},
{
"path": ".claude/skills/react-best-practices/rules/js-tosorted-immutable.md",
"chars": 1782,
"preview": "---\ntitle: Use toSorted() Instead of sort() for Immutability\nimpact: MEDIUM-HIGH\nimpactDescription: prevents mutation bu"
},
{
"path": ".claude/skills/react-best-practices/rules/rendering-activity.md",
"chars": 564,
"preview": "---\ntitle: Use Activity Component for Show/Hide\nimpact: MEDIUM\nimpactDescription: preserves state/DOM\ntags: rendering, a"
},
{
"path": ".claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md",
"chars": 1185,
"preview": "---\ntitle: Animate SVG Wrapper Instead of SVG Element\nimpact: LOW\nimpactDescription: enables hardware acceleration\ntags:"
},
{
"path": ".claude/skills/react-best-practices/rules/rendering-conditional-render.md",
"chars": 980,
"preview": "---\ntitle: Use Explicit Conditional Rendering\nimpact: LOW\nimpactDescription: prevents rendering 0 or NaN\ntags: rendering"
},
{
"path": ".claude/skills/react-best-practices/rules/rendering-content-visibility.md",
"chars": 814,
"preview": "---\ntitle: CSS content-visibility for Long Lists\nimpact: HIGH\nimpactDescription: faster initial render\ntags: rendering, "
},
{
"path": ".claude/skills/react-best-practices/rules/rendering-hoist-jsx.md",
"chars": 1039,
"preview": "---\ntitle: Hoist Static JSX Elements\nimpact: LOW\nimpactDescription: avoids re-creation\ntags: rendering, jsx, static, opt"
},
{
"path": ".claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md",
"chars": 2308,
"preview": "---\ntitle: Prevent Hydration Mismatch Without Flickering\nimpact: MEDIUM\nimpactDescription: avoids visual flicker and hyd"
},
{
"path": ".claude/skills/react-best-practices/rules/rendering-svg-precision.md",
"chars": 588,
"preview": "---\ntitle: Optimize SVG Precision\nimpact: LOW\nimpactDescription: reduces file size\ntags: rendering, svg, optimization, s"
},
{
"path": ".claude/skills/react-best-practices/rules/rerender-defer-reads.md",
"chars": 973,
"preview": "---\ntitle: Defer State Reads to Usage Point\nimpact: MEDIUM\nimpactDescription: avoids unnecessary subscriptions\ntags: rer"
},
{
"path": ".claude/skills/react-best-practices/rules/rerender-dependencies.md",
"chars": 824,
"preview": "---\ntitle: Narrow Effect Dependencies\nimpact: LOW\nimpactDescription: minimizes effect re-runs\ntags: rerender, useEffect,"
},
{
"path": ".claude/skills/react-best-practices/rules/rerender-derived-state.md",
"chars": 724,
"preview": "---\ntitle: Subscribe to Derived State\nimpact: MEDIUM\nimpactDescription: reduces re-render frequency\ntags: rerender, deri"
},
{
"path": ".claude/skills/react-best-practices/rules/rerender-functional-setstate.md",
"chars": 2958,
"preview": "---\ntitle: Use Functional setState Updates\nimpact: MEDIUM\nimpactDescription: prevents stale closures and unnecessary cal"
},
{
"path": ".claude/skills/react-best-practices/rules/rerender-lazy-state-init.md",
"chars": 2016,
"preview": "---\ntitle: Use Lazy State Initialization\nimpact: MEDIUM\nimpactDescription: wasted computation on every render\ntags: reac"
},
{
"path": ".claude/skills/react-best-practices/rules/rerender-memo.md",
"chars": 1148,
"preview": "---\ntitle: Extract to Memoized Components\nimpact: MEDIUM\nimpactDescription: enables early returns\ntags: rerender, memo, "
},
{
"path": ".claude/skills/react-best-practices/rules/rerender-transitions.md",
"chars": 1055,
"preview": "---\ntitle: Use Transitions for Non-Urgent Updates\nimpact: MEDIUM\nimpactDescription: maintains UI responsiveness\ntags: re"
},
{
"path": ".claude/skills/react-best-practices/rules/server-after-nonblocking.md",
"chars": 2012,
"preview": "---\ntitle: Use after() for Non-Blocking Operations\nimpact: MEDIUM\nimpactDescription: faster response times\ntags: server,"
},
{
"path": ".claude/skills/react-best-practices/rules/server-cache-lru.md",
"chars": 1353,
"preview": "---\ntitle: Cross-Request LRU Caching\nimpact: HIGH\nimpactDescription: caches across requests\ntags: server, cache, lru, cr"
},
{
"path": ".claude/skills/react-best-practices/rules/server-cache-react.md",
"chars": 681,
"preview": "---\ntitle: Per-Request Deduplication with React.cache()\nimpact: MEDIUM\nimpactDescription: deduplicates within request\nta"
},
{
"path": ".claude/skills/react-best-practices/rules/server-parallel-fetching.md",
"chars": 1515,
"preview": "---\ntitle: Parallel Data Fetching with Component Composition\nimpact: CRITICAL\nimpactDescription: eliminates server-side "
},
{
"path": ".claude/skills/react-best-practices/rules/server-serialization.md",
"chars": 996,
"preview": "---\ntitle: Minimize Serialization at RSC Boundaries\nimpact: HIGH\nimpactDescription: reduces data transfer size\ntags: ser"
},
{
"path": ".dockerignore",
"chars": 63,
"preview": "node_modules\n.next\ndist\n.turbo\n.git\n*.log\n.env*\n!.env.template\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 17,
"preview": "github: ridafkih\n"
},
{
"path": ".github/workflows/checks.yml",
"chars": 821,
"preview": "name: Check Code Standards\n\non:\n pull_request:\n branches: [main]\n\njobs:\n types:\n runs-on: ubuntu-latest\n step"
},
{
"path": ".github/workflows/docker-publish.yml",
"chars": 6808,
"preview": "name: Publish Docker Images\n\non:\n push:\n tags:\n - \"v*.*.*\"\n\nconcurrency:\n group: publish-${{ github.ref }}\n c"
},
{
"path": ".gitignore",
"chars": 102,
"preview": "node_modules\n.env\n.idea\n.DS_Store\n.turbo/\ndist/\n*.tsbuildinfo\nscreenshots/\napplications/mobile/\n.pki/\n"
},
{
"path": ".oxlintrc.json",
"chars": 2276,
"preview": "{\n \"$schema\": \"./node_modules/oxlint/configuration_schema.json\",\n \"plugins\": [\"import\", \"typescript\", \"unicorn\", \"prom"
},
{
"path": "Caddyfile",
"chars": 127,
"preview": "keeper.localhost {\n\ttls internal\n\n\treverse_proxy host.docker.internal:5173\n}\n\n:80 {\n\treverse_proxy host.docker.internal:"
},
{
"path": "LICENSE",
"chars": 34520,
"preview": " GNU AFFERO GENERAL PUBLIC LICENSE\n Version 3, 19 November 2007\n\n Copyright (C)"
},
{
"path": "NOTICE",
"chars": 278,
"preview": "Copyright (C) 2025 Rida F'kih\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms o"
},
{
"path": "README.md",
"chars": 31531,
"preview": "\n\n# About\n\nKeeper is a simple & open-source calendar syncing tool. It allow"
},
{
"path": "applications/web/.eslintrc.cjs",
"chars": 436,
"preview": "module.exports = {\n root: true,\n env: { browser: true, es2020: true },\n extends: [\n 'eslint:recommended',\n 'plu"
},
{
"path": "applications/web/.gitignore",
"chars": 253,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
},
{
"path": "applications/web/Dockerfile",
"chars": 937,
"preview": "FROM oven/bun:1 AS base\nWORKDIR /app\n\nFROM base AS source\nCOPY . .\n\nFROM base AS prune\nRUN bun install --global turbo\nCO"
},
{
"path": "applications/web/README.md",
"chars": 1300,
"preview": "# React + TypeScript + Vite\n\nThis template provides a minimal setup to get React working in Vite with HMR and some ESLin"
},
{
"path": "applications/web/entrypoint.sh",
"chars": 79,
"preview": "#!/bin/sh\nset -e\n\nexec bun dist/server-entry/index.js 2>&1 | keeper-otelemetry\n"
},
{
"path": "applications/web/eslint.config.js",
"chars": 897,
"preview": "import js from \"@eslint/js\";\nimport tseslint from \"@typescript-eslint/eslint-plugin\";\nimport tsparser from \"@typescript-"
},
{
"path": "applications/web/index.html",
"chars": 203,
"preview": "<!doctype html>\n<html>\n <head>\n <link rel=\"stylesheet\" href=\"/src/index.css\" />\n </head>\n <body>\n <div id=\"root"
},
{
"path": "applications/web/package.json",
"chars": 1874,
"preview": "{\n \"name\": \"@keeper.sh/web\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"bu"
},
{
"path": "applications/web/plugins/blog.ts",
"chars": 5062,
"preview": "import { readFileSync, readdirSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport type { Plugin } f"
},
{
"path": "applications/web/plugins/sitemap.ts",
"chars": 2467,
"preview": "import { readdirSync, readFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport type { Plugin } f"
},
{
"path": "applications/web/public/llms-full.txt",
"chars": 6066,
"preview": "# Keeper.sh — Full Context\n\n> Open-source calendar event syncing. Synchronize events between your personal, work, busine"
},
{
"path": "applications/web/public/llms.txt",
"chars": 1274,
"preview": "# Keeper.sh\n\n> Open-source calendar event syncing. Synchronize events between your personal, work, business and school c"
},
{
"path": "applications/web/public/robots.txt",
"chars": 2201,
"preview": "User-agent: *\nAllow: /\nDisallow: /dashboard\nDisallow: /auth\nDisallow: /login\nDisallow: /register\nDisallow: /verify-email"
},
{
"path": "applications/web/public/site.webmanifest",
"chars": 520,
"preview": "{\n \"name\": \"Keeper.sh\",\n \"short_name\": \"Keeper\",\n \"start_url\": \"/\",\n \"icons\": [\n {\n \"src\": \"/180x180-light-o"
},
{
"path": "applications/web/scripts/build.ts",
"chars": 310,
"preview": "import { build } from \"bun\";\n\nawait build({\n entrypoints: [\"src/server/index.ts\"],\n outdir: \"./dist/server-entry\",\n t"
},
{
"path": "applications/web/scripts/start.ts",
"chars": 350,
"preview": "import { existsSync } from \"node:fs\";\n\nconst serverEntryUrl = new URL(\"../dist/server-entry/index.js\", import.meta.url);"
},
{
"path": "applications/web/src/components/analytics-scripts.tsx",
"chars": 2966,
"preview": "import { useCallback, useEffect, useRef, useSyncExternalStore } from \"react\";\nimport { useLocation } from \"@tanstack/rea"
},
{
"path": "applications/web/src/components/cookie-consent.tsx",
"chars": 3642,
"preview": "import { useCallback, useState } from \"react\";\nimport type { PropsWithChildren } from \"react\";\nimport { AnimatePresence,"
},
{
"path": "applications/web/src/components/ui/composites/navigation-menu/navigation-menu-editable.tsx",
"chars": 9326,
"preview": "import { use, useEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent, type ReactNode } from \"react\";\nimpo"
},
{
"path": "applications/web/src/components/ui/composites/navigation-menu/navigation-menu-items.tsx",
"chars": 7411,
"preview": "import type { ComponentPropsWithoutRef, PropsWithChildren } from \"react\";\nimport { use } from \"react\";\nimport { Link } f"
},
{
"path": "applications/web/src/components/ui/composites/navigation-menu/navigation-menu-popover.tsx",
"chars": 5834,
"preview": "import { use, useCallback, useEffect, useRef, useState, type PropsWithChildren, type ReactNode } from \"react\";\nimport { "
},
{
"path": "applications/web/src/components/ui/composites/navigation-menu/navigation-menu.contexts.ts",
"chars": 799,
"preview": "import { createContext, use, type ReactNode } from \"react\";\nimport type { MenuVariant } from \"./navigation-menu.styles\";"
},
{
"path": "applications/web/src/components/ui/composites/navigation-menu/navigation-menu.styles.ts",
"chars": 3914,
"preview": "import { tv, type VariantProps } from \"tailwind-variants/lite\";\n\nexport const navigationMenuStyle = tv({\n base: \"flex f"
},
{
"path": "applications/web/src/components/ui/primitives/animated-reveal.tsx",
"chars": 968,
"preview": "import { AnimatePresence, LazyMotion } from \"motion/react\";\nimport { loadMotionFeatures } from \"@/lib/motion-features\";\n"
},
{
"path": "applications/web/src/components/ui/primitives/back-button.tsx",
"chars": 957,
"preview": "import { useRouter, useCanGoBack, useNavigate } from \"@tanstack/react-router\";\nimport ArrowLeft from \"lucide-react/dist/"
},
{
"path": "applications/web/src/components/ui/primitives/button.tsx",
"chars": 2590,
"preview": "import type { ComponentPropsWithoutRef, PropsWithChildren } from \"react\";\nimport { Link } from \"@tanstack/react-router\";"
},
{
"path": "applications/web/src/components/ui/primitives/checkbox.tsx",
"chars": 2175,
"preview": "import type { ReactNode } from \"react\";\nimport Check from \"lucide-react/dist/esm/icons/check\";\nimport { tv } from \"tailw"
},
{
"path": "applications/web/src/components/ui/primitives/collapsible.tsx",
"chars": 1578,
"preview": "import { type PropsWithChildren, type ReactNode } from \"react\";\nimport { tv } from \"tailwind-variants/lite\";\n\nconst coll"
},
{
"path": "applications/web/src/components/ui/primitives/dashboard-heading.tsx",
"chars": 1936,
"preview": "import type { PropsWithChildren, ReactNode } from \"react\";\nimport { tv } from \"tailwind-variants/lite\";\nimport { Text } "
},
{
"path": "applications/web/src/components/ui/primitives/delete-confirmation.tsx",
"chars": 1383,
"preview": "import LoaderCircle from \"lucide-react/dist/esm/icons/loader-circle\";\nimport { Button, ButtonText } from \"./button\";\nimp"
},
{
"path": "applications/web/src/components/ui/primitives/divider.tsx",
"chars": 581,
"preview": "import type { PropsWithChildren } from \"react\";\n\nconst dashedLine = \"h-px grow bg-[repeating-linear-gradient(to_right,va"
},
{
"path": "applications/web/src/components/ui/primitives/error-state.tsx",
"chars": 586,
"preview": "import { Text } from \"./text\";\nimport { Button, ButtonText } from \"./button\";\n\ninterface ErrorStateProps {\n message?: s"
},
{
"path": "applications/web/src/components/ui/primitives/fade-in.tsx",
"chars": 1344,
"preview": "import type { PropsWithChildren } from \"react\";\nimport { LazyMotion, type HTMLMotionProps, type TargetAndTransition } fr"
},
{
"path": "applications/web/src/components/ui/primitives/github-star-button.tsx",
"chars": 3878,
"preview": "import { AnimatePresence } from \"motion/react\";\nimport Star from \"lucide-react/dist/esm/icons/star\";\nimport {\n Componen"
},
{
"path": "applications/web/src/components/ui/primitives/heading.tsx",
"chars": 1254,
"preview": "import type { PropsWithChildren } from \"react\";\nimport { tv } from \"tailwind-variants/lite\";\n\nconst heading = tv({\n bas"
},
{
"path": "applications/web/src/components/ui/primitives/input.tsx",
"chars": 909,
"preview": "import type { ComponentPropsWithoutRef, Ref } from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants/lit"
},
{
"path": "applications/web/src/components/ui/primitives/list.tsx",
"chars": 1300,
"preview": "import type { ComponentPropsWithoutRef, PropsWithChildren } from \"react\";\n\ntype ListProps = PropsWithChildren<ComponentP"
},
{
"path": "applications/web/src/components/ui/primitives/markdown-component-map.ts",
"chars": 875,
"preview": "import type { Components } from \"streamdown\";\nimport {\n MarkdownBlockquote,\n MarkdownCodeBlock,\n MarkdownHeadingOne,\n"
},
{
"path": "applications/web/src/components/ui/primitives/markdown-components.tsx",
"chars": 3756,
"preview": "import type { JSX } from \"react\";\nimport { Heading1, Heading2, Heading3 } from \"./heading\";\nimport { ListItem, OrderedLi"
},
{
"path": "applications/web/src/components/ui/primitives/modal.tsx",
"chars": 2797,
"preview": "import type { PropsWithChildren } from \"react\";\nimport { createContext, use, useEffect, useRef, useState } from \"react\";"
},
{
"path": "applications/web/src/components/ui/primitives/pagination.tsx",
"chars": 1393,
"preview": "import type { PropsWithChildren } from \"react\";\nimport ChevronLeft from \"lucide-react/dist/esm/icons/chevron-left\";\nimpo"
},
{
"path": "applications/web/src/components/ui/primitives/provider-icon-stack.tsx",
"chars": 2562,
"preview": "import { AnimatePresence, LazyMotion } from \"motion/react\";\nimport { loadMotionFeatures } from \"@/lib/motion-features\";\n"
},
{
"path": "applications/web/src/components/ui/primitives/provider-icon.tsx",
"chars": 857,
"preview": "import Calendar from \"lucide-react/dist/esm/icons/calendar\";\nimport LinkIcon from \"lucide-react/dist/esm/icons/link\";\nim"
},
{
"path": "applications/web/src/components/ui/primitives/shimmer-text.tsx",
"chars": 610,
"preview": "import type { PropsWithChildren } from \"react\";\nimport { cn } from \"@/utils/cn\";\n\ntype ShimmerTextProps = PropsWithChild"
},
{
"path": "applications/web/src/components/ui/primitives/staggered-backdrop-blur.tsx",
"chars": 1482,
"preview": "const LAYERS = [\n { z: 1, mask: \"linear-gradient(to top, rgba(0,0,0,0) 0%, rgb(0,0,0) 12.5%, rgb(0,0,0) 25%, rgba(0,0,0"
},
{
"path": "applications/web/src/components/ui/primitives/template-text.tsx",
"chars": 1059,
"preview": "import { tv } from \"tailwind-variants/lite\";\nimport { parseTemplate } from \"@/utils/templates\";\n\nconst templateVariable "
},
{
"path": "applications/web/src/components/ui/primitives/text-link.tsx",
"chars": 1414,
"preview": "import type { ComponentPropsWithoutRef, PropsWithChildren } from \"react\";\nimport { Link } from \"@tanstack/react-router\";"
},
{
"path": "applications/web/src/components/ui/primitives/text.tsx",
"chars": 1238,
"preview": "import type { CSSProperties, PropsWithChildren } from \"react\";\nimport { tv } from \"tailwind-variants/lite\";\n\nconst text "
},
{
"path": "applications/web/src/components/ui/primitives/tooltip.tsx",
"chars": 1633,
"preview": "import { type PropsWithChildren, type ReactNode, useRef, useState, useCallback } from \"react\";\nimport { createPortal } f"
},
{
"path": "applications/web/src/components/ui/primitives/upgrade-hint.tsx",
"chars": 1314,
"preview": "import type { PropsWithChildren, ReactNode } from \"react\";\nimport { Link } from \"@tanstack/react-router\";\nimport { getCo"
},
{
"path": "applications/web/src/components/ui/shells/layout.tsx",
"chars": 711,
"preview": "import type { PropsWithChildren } from \"react\";\nimport { cn } from \"@/utils/cn\";\n\nconst GRID_COLS = \"grid grid-cols-[min"
},
{
"path": "applications/web/src/components/ui/shells/route-shell.tsx",
"chars": 975,
"preview": "import LoaderCircle from \"lucide-react/dist/esm/icons/loader-circle\";\nimport { BackButton } from \"@/components/ui/primit"
},
{
"path": "applications/web/src/components/ui/shells/session-slot.tsx",
"chars": 704,
"preview": "import { type ReactNode, useSyncExternalStore } from \"react\";\nimport { useRouteContext } from \"@tanstack/react-router\";\n"
},
{
"path": "applications/web/src/config/commercial.ts",
"chars": 144,
"preview": "import { getPublicRuntimeConfig } from \"@/lib/runtime-config\";\n\nexport const getCommercialMode = () => getPublicRuntimeC"
},
{
"path": "applications/web/src/config/gdpr.ts",
"chars": 389,
"preview": "/** ISO 3166-1 alpha-2 country codes for EU/EEA + UK where GDPR applies. */\nconst GDPR_COUNTRIES = new Set([\n \"AT\",\n \""
},
{
"path": "applications/web/src/config/plans.ts",
"chars": 1567,
"preview": "import type { PublicRuntimeConfig } from \"@/lib/runtime-config\";\n\nexport interface PlanConfig {\n id: \"free\" | \"pro\";\n "
},
{
"path": "applications/web/src/content/blog/introducing-keeper-blog.mdx",
"chars": 10057,
"preview": "---\ntitle: \"Why I Built an Open-Source Calendar Syncing Tool\"\ndescription: \"Why I built an open-source calendar syncing "
},
{
"path": "applications/web/src/features/auth/components/auth-form.tsx",
"chars": 17198,
"preview": "import { useEffect, useRef, type Ref, type SubmitEvent } from \"react\";\nimport { useNavigate } from \"@tanstack/react-rout"
},
{
"path": "applications/web/src/features/auth/components/auth-switch-prompt.tsx",
"chars": 269,
"preview": "import type { PropsWithChildren } from \"react\";\nimport { Text } from \"@/components/ui/primitives/text\";\n\nexport function"
},
{
"path": "applications/web/src/features/auth/components/caldav-connect-form.tsx",
"chars": 7003,
"preview": "import { useRef, useState, useTransition } from \"react\";\nimport { useNavigate } from \"@tanstack/react-router\";\nimport Lo"
},
{
"path": "applications/web/src/features/auth/components/caldav-connect-page.tsx",
"chars": 1119,
"preview": "import type { ReactNode } from \"react\";\nimport { Heading2 } from \"@/components/ui/primitives/heading\";\nimport { Text } f"
},
{
"path": "applications/web/src/features/auth/components/ics-connect-form.tsx",
"chars": 4105,
"preview": "import { useState, useTransition, type SubmitEvent } from \"react\";\nimport LoaderCircle from \"lucide-react/dist/esm/icons"
},
{
"path": "applications/web/src/features/auth/components/oauth-preamble.tsx",
"chars": 4917,
"preview": "import type { ReactNode, SubmitEvent } from \"react\";\nimport ArrowLeftRight from \"lucide-react/dist/esm/icons/arrow-left-"
},
{
"path": "applications/web/src/features/blog/components/blog-post-cta.tsx",
"chars": 1335,
"preview": "import { ButtonText, LinkButton } from \"@/components/ui/primitives/button\";\nimport { Heading3 } from \"@/components/ui/pr"
},
{
"path": "applications/web/src/features/dashboard/components/event-graph.tsx",
"chars": 9405,
"preview": "import { memo, useCallback, useEffect, useMemo, useRef } from \"react\";\nimport { atom } from \"jotai\";\nimport { useAtomVal"
},
{
"path": "applications/web/src/features/dashboard/components/metadata-row.tsx",
"chars": 1207,
"preview": "import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport type { Link } from \"@tanstack/react-router\";\nim"
},
{
"path": "applications/web/src/features/dashboard/components/sync-status-helpers.ts",
"chars": 734,
"preview": "import type { CompositeSyncState } from \"@/state/sync\";\n\nconst clampPercent = (value: number): number => {\n if (!Number"
},
{
"path": "applications/web/src/features/dashboard/components/sync-status.tsx",
"chars": 2412,
"preview": "import { useAtomValue } from \"jotai\";\nimport { syncStateAtom, syncStatusLabelAtom, syncStatusShimmerAtom } from \"@/state"
},
{
"path": "applications/web/src/features/dashboard/components/upgrade-card.tsx",
"chars": 2882,
"preview": "import type { PropsWithChildren } from \"react\";\nimport { tv } from \"tailwind-variants/lite\";\n\nconst upgradeCard = tv({\n "
},
{
"path": "applications/web/src/features/marketing/components/marketing-cta.tsx",
"chars": 800,
"preview": "import type { PropsWithChildren } from \"react\";\n\nexport function MarketingCtaSection({ children }: PropsWithChildren) {\n"
},
{
"path": "applications/web/src/features/marketing/components/marketing-faq.tsx",
"chars": 784,
"preview": "import type { PropsWithChildren } from \"react\";\nimport { Text } from \"@/components/ui/primitives/text\";\n\nexport function"
},
{
"path": "applications/web/src/features/marketing/components/marketing-feature-bento.tsx",
"chars": 1689,
"preview": "import type { PropsWithChildren } from \"react\";\nimport { cn } from \"@/utils/cn\";\n\ntype MarketingFeatureBentoCardProps = "
},
{
"path": "applications/web/src/features/marketing/components/marketing-footer.tsx",
"chars": 1630,
"preview": "import type { PropsWithChildren } from \"react\";\nimport { Link } from \"@tanstack/react-router\";\n\nexport function Marketin"
},
{
"path": "applications/web/src/features/marketing/components/marketing-header.tsx",
"chars": 1004,
"preview": "import type { PropsWithChildren } from \"react\";\nimport { Link } from \"@tanstack/react-router\";\nimport { LayoutRow } from"
},
{
"path": "applications/web/src/features/marketing/components/marketing-how-it-works.tsx",
"chars": 2433,
"preview": "import type { PropsWithChildren } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Text } from \"@/components/ui/p"
},
{
"path": "applications/web/src/features/marketing/components/marketing-illustration-calendar.tsx",
"chars": 3124,
"preview": "import { useAtomValue } from \"jotai\";\nimport { LazyMotion } from \"motion/react\";\nimport { loadMotionFeatures } from \"@/l"
},
{
"path": "applications/web/src/features/marketing/components/marketing-pricing-section.tsx",
"chars": 5655,
"preview": "import type { PropsWithChildren } from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants/lite\";\nimport {"
},
{
"path": "applications/web/src/features/marketing/contributors.json",
"chars": 788,
"preview": "[\n {\n \"username\": \"ridafkih\",\n \"name\": \"Rida F'kih\",\n \"avatarUrl\": \"/contributors/ridafkih.webp\"\n },\n {\n "
},
{
"path": "applications/web/src/generated/tanstack/route-tree.generated.ts",
"chars": 50108,
"preview": "/* eslint-disable */\n\n// @ts-nocheck\n\n// noinspection JSUnusedGlobalSymbols\n\n// This file was automatically generated by"
},
{
"path": "applications/web/src/hooks/use-animated-swr.ts",
"chars": 473,
"preview": "import { useState } from \"react\";\nimport useSWR from \"swr\";\nimport type { SWRConfiguration, SWRResponse } from \"swr\";\n\ni"
},
{
"path": "applications/web/src/hooks/use-api-tokens.ts",
"chars": 873,
"preview": "import useSWR from \"swr\";\nimport { fetcher, apiFetch } from \"@/lib/fetcher\";\n\nexport interface ApiToken {\n id: string;\n"
},
{
"path": "applications/web/src/hooks/use-entitlements.ts",
"chars": 2262,
"preview": "import { useMemo, useCallback } from \"react\";\nimport useSWR, { useSWRConfig } from \"swr\";\nimport { fetcher } from \"@/lib"
},
{
"path": "applications/web/src/hooks/use-events.ts",
"chars": 2157,
"preview": "import useSWRInfinite from \"swr/infinite\";\nimport { fetcher } from \"@/lib/fetcher\";\nimport { useStartOfToday } from \"./u"
},
{
"path": "applications/web/src/hooks/use-has-password.ts",
"chars": 351,
"preview": "import useSWR from \"swr\";\nimport { authClient } from \"@/lib/auth-client\";\n\nconst fetchHasPassword = async (): Promise<bo"
},
{
"path": "applications/web/src/hooks/use-passkeys.ts",
"chars": 617,
"preview": "import useSWR from \"swr\";\nimport { authClient } from \"@/lib/auth-client\";\n\nexport interface Passkey {\n id: string;\n na"
},
{
"path": "applications/web/src/hooks/use-session.ts",
"chars": 780,
"preview": "import useSWR from \"swr\";\nimport { authClient } from \"@/lib/auth-client\";\n\nexport interface SessionUser {\n id: string;\n"
},
{
"path": "applications/web/src/hooks/use-start-of-today.ts",
"chars": 740,
"preview": "import { useEffect, useState } from \"react\";\n\nfunction resolveStartOfToday(): Date {\n const date = new Date();\n date.s"
},
{
"path": "applications/web/src/hooks/use-subscription.ts",
"chars": 1925,
"preview": "import useSWR from \"swr\";\nimport { fetcher } from \"@/lib/fetcher\";\nimport { getCommercialMode } from \"@/config/commercia"
},
{
"path": "applications/web/src/illustrations/how-it-works-configure.tsx",
"chars": 1384,
"preview": "function MiniToggle({ checked }: { checked: boolean }) {\n return (\n <div\n className={`w-8 h-4.5 rounded-full sh"
},
{
"path": "applications/web/src/illustrations/how-it-works-connect.tsx",
"chars": 1262,
"preview": "import ArrowRightIcon from \"lucide-react/dist/esm/icons/arrow-right\";\n\nconst PROVIDERS = [\n { icon: \"/integrations/icon"
},
{
"path": "applications/web/src/illustrations/how-it-works-sync.tsx",
"chars": 3268,
"preview": "import { useEffect, useRef, useState } from \"react\";\nimport { LazyMotion } from \"motion/react\";\nimport * as m from \"moti"
},
{
"path": "applications/web/src/illustrations/marketing-illustration-contributors.tsx",
"chars": 3645,
"preview": "import { AnimatePresence, LazyMotion } from \"motion/react\";\nimport * as m from \"motion/react-m\";\nimport { loadMotionFeat"
},
{
"path": "applications/web/src/illustrations/marketing-illustration-providers.tsx",
"chars": 1783,
"preview": "import { providerIcons } from \"@/lib/providers\";\n\nconst ORBIT_ITEMS = Object.entries(providerIcons).reverse();\nconst ORB"
},
{
"path": "applications/web/src/illustrations/marketing-illustration-setup.tsx",
"chars": 4443,
"preview": "import { AnimatePresence, LazyMotion } from \"motion/react\";\nimport * as m from \"motion/react-m\";\nimport { loadMotionFeat"
},
{
"path": "applications/web/src/illustrations/marketing-illustration-sync.tsx",
"chars": 1865,
"preview": "const R = 12;\nconst PATH_UPPER = `M -10,20 L ${210 - R},20 Q 210,20 210,${20 + R} L 210,${50 - R} Q 210,50 ${210 + R},50"
},
{
"path": "applications/web/src/index.css",
"chars": 5895,
"preview": "@import \"tailwindcss\";\n\n@custom-variant pointer-hover (@media (hover: hover));\n\nhtml {\n @apply w-full min-h-full bg-bac"
},
{
"path": "applications/web/src/index.d.ts",
"chars": 150,
"preview": "declare module \"lucide-react/dist/esm/icons/*\" {\n import type { LucideIcon } from \"lucide-react\";\n const icon: LucideI"
},
{
"path": "applications/web/src/lib/analytics.ts",
"chars": 4727,
"preview": "import type { PublicRuntimeConfig } from \"./runtime-config\";\n\nconst CONSENT_COOKIE = \"keeper.analytics_consent\";\nconst C"
},
{
"path": "applications/web/src/lib/auth-capabilities.ts",
"chars": 2089,
"preview": "import { authCapabilitiesSchema } from \"@keeper.sh/data-schemas\";\nimport type { AuthCapabilities } from \"@keeper.sh/data"
},
{
"path": "applications/web/src/lib/auth-client.ts",
"chars": 239,
"preview": "import { createAuthClient } from \"better-auth/react\";\nimport { passkeyClient } from \"@better-auth/passkey/client\";\n\nexpo"
},
{
"path": "applications/web/src/lib/auth.ts",
"chars": 3279,
"preview": "import { authClient } from \"./auth-client\";\nimport type { AuthCapabilities } from \"./auth-capabilities\";\n\nasync function"
},
{
"path": "applications/web/src/lib/blog-posts.ts",
"chars": 1006,
"preview": "// @ts-expect-error - virtual module provided by plugins/blog.ts\nimport { blogPosts as processedPosts } from \"virtual:bl"
},
{
"path": "applications/web/src/lib/fetcher.ts",
"chars": 668,
"preview": "export class HttpError extends Error {\n constructor(\n public readonly status: number,\n public readonly url: strin"
},
{
"path": "applications/web/src/lib/mcp-auth-flow.ts",
"chars": 3032,
"preview": "import { type } from \"arktype\";\n\ntype SearchParams = Record<string, unknown>;\ntype StringSearchParams = Record<string, s"
},
{
"path": "applications/web/src/lib/motion-features.ts",
"chars": 92,
"preview": "export const loadMotionFeatures = () =>\n import(\"motion/react\").then((mod) => mod.domMin);\n"
},
{
"path": "applications/web/src/lib/page-metadata.ts",
"chars": 511,
"preview": "const monthYearFormatter = new Intl.DateTimeFormat(\"en-US\", {\n month: \"long\",\n year: \"numeric\",\n timeZone: \"UTC\",\n});"
},
{
"path": "applications/web/src/lib/pluralize.ts",
"chars": 202,
"preview": "export function pluralize(count: number, singular: string, plural: string = `${singular}s`): string {\n if (count === 1)"
},
{
"path": "applications/web/src/lib/providers.ts",
"chars": 400,
"preview": "export const providerIcons: Record<string, string> = {\n google: \"/integrations/icon-google-calendar.svg\",\n outlook: \"/"
},
{
"path": "applications/web/src/lib/route-access-guards.ts",
"chars": 668,
"preview": "export type RedirectTarget = \"/dashboard\" | \"/login\";\n\nexport type SubscriptionPlan = \"free\" | \"pro\";\n\nexport const reso"
},
{
"path": "applications/web/src/lib/router-context.ts",
"chars": 639,
"preview": "import type { PublicRuntimeConfig } from \"./runtime-config\";\n\nexport interface AppAuthContext {\n hasSession: () => bool"
},
{
"path": "applications/web/src/lib/runtime-config.ts",
"chars": 3116,
"preview": "import { GDPR_COUNTRIES } from \"@/config/gdpr\";\n\ninterface PublicRuntimeConfig {\n commercialMode: boolean;\n gdprApplie"
},
{
"path": "applications/web/src/lib/seo.ts",
"chars": 5394,
"preview": "const SITE_URL = \"https://keeper.sh\";\nconst SITE_NAME = \"Keeper.sh\";\n\nexport function canonicalUrl(path: string): string"
},
{
"path": "applications/web/src/lib/serialized-mutate.ts",
"chars": 2497,
"preview": "type ErrorHandler = (error: unknown) => void;\n\ninterface QueuedPatch {\n patch: Record<string, unknown>;\n flush: (merge"
},
{
"path": "applications/web/src/lib/session-cookie.ts",
"chars": 351,
"preview": "const SESSION_COOKIE = \"keeper.has_session=1\";\n\nexport function hasSessionCookie(cookieHeader?: string): boolean {\n con"
},
{
"path": "applications/web/src/lib/swr.ts",
"chars": 425,
"preview": "import type { ScopedMutator } from \"swr\";\n\n/**\n * Revalidate all account and source caches.\n * Use after creating, updat"
},
{
"path": "applications/web/src/lib/time.ts",
"chars": 2361,
"preview": "const MINS_PER_HOUR = 60;\nconst HOURS_PER_DAY = 24;\nconst MINS_PER_DAY = MINS_PER_HOUR * HOURS_PER_DAY;\n\nconst getSuffix"
},
{
"path": "applications/web/src/main.tsx",
"chars": 682,
"preview": "import { StrictMode } from \"react\";\nimport { createRoot, hydrateRoot } from \"react-dom/client\";\nimport { RouterProvider "
},
{
"path": "applications/web/src/providers/sync-provider-logic.ts",
"chars": 2578,
"preview": "import { isSocketMessage, isSyncAggregate } from \"@keeper.sh/data-schemas/client\";\nimport type { CompositeSyncState, Syn"
},
{
"path": "applications/web/src/providers/sync-provider.tsx",
"chars": 7174,
"preview": "import { useEffect } from \"react\";\nimport { useSetAtom } from \"jotai\";\nimport { syncStateAtom, type CompositeSyncState }"
},
{
"path": "applications/web/src/routeTree.gen.ts",
"chars": 49814,
"preview": "/* eslint-disable */\n\n// @ts-nocheck\n\n// noinspection JSUnusedGlobalSymbols\n\n// This file was automatically generated by"
},
{
"path": "applications/web/src/router.ts",
"chars": 3994,
"preview": "import { createRouter } from \"@tanstack/react-router\";\nimport { routeTree } from \"./generated/tanstack/route-tree.genera"
},
{
"path": "applications/web/src/routes/(auth)/forgot-password.tsx",
"chars": 3383,
"preview": "import { useState } from \"react\";\nimport { createFileRoute, redirect } from \"@tanstack/react-router\";\nimport Mail from \""
},
{
"path": "applications/web/src/routes/(auth)/login.tsx",
"chars": 1065,
"preview": "import { createFileRoute } from \"@tanstack/react-router\";\nimport { AuthForm, type AuthScreenCopy } from \"@/features/auth"
},
{
"path": "applications/web/src/routes/(auth)/register.tsx",
"chars": 1077,
"preview": "import { createFileRoute } from \"@tanstack/react-router\";\nimport { AuthForm, type AuthScreenCopy } from \"@/features/auth"
},
{
"path": "applications/web/src/routes/(auth)/reset-password.tsx",
"chars": 4654,
"preview": "import { useState } from \"react\";\nimport { createFileRoute, redirect } from \"@tanstack/react-router\";\nimport CircleCheck"
},
{
"path": "applications/web/src/routes/(auth)/route.tsx",
"chars": 861,
"preview": "import { createFileRoute, Outlet, redirect } from \"@tanstack/react-router\";\nimport { getMcpAuthorizationSearch } from \"@"
},
{
"path": "applications/web/src/routes/(auth)/verify-authentication.tsx",
"chars": 650,
"preview": "import { useEffect } from \"react\";\nimport { createFileRoute, useNavigate } from \"@tanstack/react-router\";\nimport { useSe"
},
{
"path": "applications/web/src/routes/(auth)/verify-email.tsx",
"chars": 2877,
"preview": "import { useState } from \"react\";\nimport { createFileRoute, redirect } from \"@tanstack/react-router\";\nimport Mail from \""
},
{
"path": "applications/web/src/routes/(dashboard)/dashboard/accounts/$accountId.$calendarId.tsx",
"chars": 22588,
"preview": "import { use, useEffect, useMemo } from \"react\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport useSWR"
},
{
"path": "applications/web/src/routes/(dashboard)/dashboard/accounts/$accountId.index.tsx",
"chars": 6649,
"preview": "import { useEffect, useMemo, useState, useTransition } from \"react\";\nimport { createFileRoute, useNavigate } from \"@tans"
},
{
"path": "applications/web/src/routes/(dashboard)/dashboard/accounts/$accountId.setup.tsx",
"chars": 18799,
"preview": "import { useState } from \"react\";\nimport { createFileRoute, useNavigate } from \"@tanstack/react-router\";\nimport useSWR f"
},
{
"path": "applications/web/src/routes/(dashboard)/dashboard/accounts/route.tsx",
"chars": 226,
"preview": "import { createFileRoute, Outlet } from \"@tanstack/react-router\";\n\nexport const Route = createFileRoute(\"/(dashboard)/da"
},
{
"path": "applications/web/src/routes/(dashboard)/dashboard/connect/index.tsx",
"chars": 5043,
"preview": "import { createFileRoute } from \"@tanstack/react-router\";\nimport Calendar from \"lucide-react/dist/esm/icons/calendar\";\ni"
},
{
"path": "applications/web/src/routes/(dashboard)/dashboard/connect/route.tsx",
"chars": 223,
"preview": "import { createFileRoute, Outlet } from \"@tanstack/react-router\";\n\nexport const Route = createFileRoute(\"/(dashboard)/da"
},
{
"path": "applications/web/src/routes/(dashboard)/dashboard/events/index.tsx",
"chars": 5335,
"preview": "import { useEffect, useRef, memo } from \"react\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport Loader"
},
{
"path": "applications/web/src/routes/(dashboard)/dashboard/feedback.tsx",
"chars": 2936,
"preview": "import { useRef, useState } from \"react\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport { BackButton "
},
{
"path": "applications/web/src/routes/(dashboard)/dashboard/ical.tsx",
"chars": 12382,
"preview": "import { use, useMemo } from \"react\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport useSWR from \"swr\""
},
{
"path": "applications/web/src/routes/(dashboard)/dashboard/index.tsx",
"chars": 9149,
"preview": "import { createFileRoute, useNavigate } from \"@tanstack/react-router\";\nimport useSWR, { preload } from \"swr\";\nimport { A"
}
]
// ... and 713 more files (download for full content)
About this extraction
This page contains the full source code of the ridafkih/keeper.sh GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 913 files (8.8 MB), approximately 2.4M tokens, and a symbol index with 1969 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.