commits
- Add PWA detection and popup OAuth flow for Android standalone mode
- Add localStorage fallback when window.opener is lost during OAuth
- Add PWA manifest with share_target configuration
- Add minimal service worker for PWA requirements
- Add share-target route handler for receiving shared URLs
Fixes #4
Features:
- Automatically send bookmarks to Instapaper when tagged with reading list tag
- Encrypted credential storage using AES-GCM encryption
- Credential validation before saving (tests Instapaper API connection)
- Settings UI with toggle, username/password fields
- Enhanced success feedback with checkmark and verification message
- Silent failure pattern: bookmark updates succeed even if Instapaper fails
Security:
- Separate ENCRYPTION_KEY environment variable for credential encryption
- AES-GCM with 256-bit key derived via PBKDF2 (100,000 iterations)
- Passwords never logged or returned in API responses
- Credentials only decrypted when actively needed
OAuth improvements:
- Fix BASE_URL auto-detection to properly handle ngrok HTTPS forwarding
- Check X-Forwarded-Proto header for correct protocol detection
- Support dynamic ngrok URLs without manual configuration
Database:
- Migration 002: Add instapaper_enabled, instapaper_username_encrypted,
instapaper_password_encrypted columns to user_settings
Testing:
- All 73 tests passing
- Unit tests for encryption round-trip
- Unit tests for Instapaper API client with mocked fetch
- Integration tests for settings API with credential validation
- Add navigation tabs in header (Bookmarks / Reading List)
- Reading List shows bookmarks filtered by a configurable tag
- Add Settings page to configure reading list tag (default: toread)
- Store user settings in Turso database with migration
- Reading List view has full-width teaser cards and tag filtering
- Add Settings link to user menu dropdown
- Add GET/PATCH /api/settings endpoints
- Include settings in initial-data response
- Upgrade @tijs/atproto-oauth to 2.4.0 (supports custom scopes in PAR)
- Add OAUTH_SCOPES constant with specific permissions for bookmark and tag collections
- Remove broad transition:generic scope in favor of explicit CRUD permissions
- Remove debug endpoint that exposed session information
- Add SSRF protection to URL enrichment (blocks private IPs, localhost, cloud metadata)
- Add output sanitization (length limits, control char removal, favicon URL validation)
- Add security headers middleware (X-Frame-Options, HSTS, etc.)
- Add 20 security tests covering all new protections
Add tests that exercise the actual feature flows using fakes
for all external dependencies (OAuth, PDS, URL fetching).
New test files:
- tests/test-helpers.ts: Mock utilities for sessions and fetch
- tests/bookmarks.test.ts: 10 tests for bookmark CRUD
- tests/enrichment.test.ts: 11 tests for URL metadata extraction
- tests/plc-resolver.test.ts: 7 tests for DID resolution
Modified lib/session.ts to add setTestSessionProvider() hook
for injecting mock sessions in tests.
Total: 35 tests, all running without network calls.
The KV caching was causing 'Deno.openKv is not a function' errors
in production. Since the app is already fast without caching,
removing it entirely simplifies the code and eliminates the errors.
- Remove kv-cache.ts utility module
- Simplify plc-resolver.ts to call PLC directory directly
- Simplify enrichment.ts to fetch metadata directly
- Remove plc-resolver.test.ts (only tested caching behavior)
Tests verify:
- getCached returns cached values when not expired
- getCached caches null values (documenting the problem)
- invalidateCache removes cached values
- PLC resolver recovers from cached null by re-fetching
- PLC resolver does not loop infinitely on persistent null
Added --unstable-kv flag to test task for Deno KV support.
The issue was that cached null values weren't being properly handled.
The fix now:
1. Catches errors from getCached and falls back to direct fetch
2. Invalidates cached nulls and re-fetches fresh data
3. Only caches successful (non-null) results after recovery
When PLC lookups failed temporarily, null values were cached for 1 hour,
causing share pages to return 'User not found' even when the user exists.
Now cached nulls are automatically invalidated and re-fetched from PLC.
- lib/kv-cache.ts: Generic cache-aside pattern using Deno KV
- lib/plc-resolver.ts: Cached DID document resolution (1h TTL)
- lib/enrichment.ts: Cached URL metadata extraction (24h TTL)
- routes/api/share.ts: Use cached PLC resolver
- routes/share/rss.ts: Use cached PLC resolver
Reduces PLC directory load and speeds up share page requests.
Pass import.meta.url from main.ts to registerStaticRoutes() so file
paths resolve correctly when routes/static.ts is in a subdirectory.
Split main.ts (1,417 lines) into modular route files:
- routes/api/bookmarks.ts - Bookmark CRUD operations
- routes/api/tags.ts - Tag CRUD operations
- routes/api/auth.ts - Session management
- routes/api/initial-data.ts - Combined data fetch
- routes/api/share.ts - Public bookmark sharing
- routes/oauth.ts - OAuth login flow
- routes/share/rss.ts - RSS feed generation
- routes/static.ts - Static files and SPA routing
- lib/route-utils.ts - Shared utilities
main.ts is now just an orchestrator (79 lines).
All files under 500 lines per project guidelines.
- Build script now outputs bundle.[hash].js with 8-char content hash
- Creates manifest.json for runtime bundle path lookup
- Automatically cleans up old bundle files on rebuild
- Cache headers: immutable (1yr) for hashed bundles, 1hr for other assets
- HTML dynamically injects correct hashed bundle path
- esbuild can't resolve npm: specifiers, use esm.sh URLs in alias
- Keep npm: specifiers in deno.json for Deno runtime type checking
- Remove jsxImportSource pragma from frontend files (configured in deno.json)
- Add all dependencies to deno.json imports with versions
- Update all files to use bare specifiers instead of inline URLs
- Remove no-import-prefix and no-unversioned-import lint exclusions
- Exclude static/ directory from formatting (bundled files)
- Update CLAUDE.md and README.md with accurate project structure
Fresh staticFiles() middleware doesn't work on Deno Deploy without
the full build process. Add explicit route to serve bundle.js.
The bundle was gitignored, causing 404s on Deno Deploy which
doesn't support build steps. Now bundle.js is committed so
it deploys correctly.
- Add build:frontend task using npm:esbuild to create bundled JS
- Upgrade all React imports from esm.sh/react to esm.sh/react@19
- Update index.html to load /static/bundle.js instead of .tsx
- Remove esbuild-wasm runtime transpilation from file-server.ts
- Add static/bundle.js to .gitignore (build artifact)
- Update deno.json with React 19 import map and build task
This fixes the frontend not loading on Deno Deploy by pre-building
the bundle at build time instead of relying on runtime transpilation.
The file server was using getProjectRoot() which looked for 'kipclip-appview'
in the file path, but on Deno Deploy paths use 'app:///' protocol.
Changed to use resolveProjectPath() which handles both:
- Local development (file://) - uses fromFileUrl to get filesystem path
- Deno Deploy (app://) - uses URL pathname directly
- OAuth initialization is now lazy (deferred until first request)
- BASE_URL can be derived from request URL if not set in environment
- Client metadata endpoint returns dynamic URLs based on derived base
- Enables OAuth to work on Deno Deploy preview/branch URLs
Files changed:
- lib/oauth-config.ts: Add initOAuth(), getOAuth(), getBaseUrl() functions
- lib/session.ts: Use getOAuth() instead of oauth import
- main.ts: Add OAuth init middleware, update routes to use getOAuth()
- tests/api.test.ts: Initialize OAuth before creating handler
- deno.json: Remove BASE_URL from test tasks
Fresh 2.x requires _fresh/server.js to set up the build cache before
calling app.handler(). The previous code called handler() at module
load time, before the cache was initialized.
- Update dev.ts to use standard Fresh Builder pattern:
- Use builder.listen(() => import('./main.ts')) callback
- Use builder.build() without app argument
- Add 'start' task for production: deno serve -A _fresh/server.js
- Add exclude pattern for _fresh directory in lint/format
- Update dev task with watch directories
- Move backend utilities to lib/ directory
- Create main.ts as Fresh entry point with all routes
- Create dev.ts for development server
- Move tests to tests/ directory
- Add mock database mode for testing
- Remove old backend/ directory
- Update deno.json with new include paths and tasks
- Add TURSO_DATABASE_URL/TURSO_AUTH_TOKEN to CI workflow
- Update outdated valTownAdapter comment in db.ts
Remove custom HTTP client workaround now that npm registry is working.
Use @libsql/client with /web export for Deno Deploy compatibility.
Replace @libsql/client/web with a minimal fetch-based HTTP client for
remote Turso connections. This avoids npm dependencies (node-fetch,
@types/node) that were causing issues with Deno Deploy builds.
- Add turso-http.ts: pure fetch implementation of Turso v2 pipeline API
- Update db.ts: use HTTP client for remote, native client for local
- Local development still uses @libsql/client for file:// databases
- Remove Val Town conditional logic from static.ts
- Use esbuild file-server directly for all environments
- Update deploy task (remove vt push)
- Update documentation for Deno Deploy
- Update to @tijs/atproto-storage@1.0.0 (sqliteAdapter)
- Auto-detect Val Town vs Deno Deploy runtime for static file serving
- Require BASE_URL environment variable (no hardcoded fallback)
- Remove Val Town-specific type definitions
- Fix ts-ignore for custom element closing tag
- Replace Val Town SQLite with Turso/libSQL for database
- Use web client for remote Turso, native client for local dev
- Add Sentry for error tracking, replacing email alerts
- Remove local-sqlite.ts (no longer needed)
- Update .env.example with Turso and Sentry vars
- Add COOKIE_SECRET to test tasks in deno.json
- Add PWA detection and popup OAuth flow for Android standalone mode
- Add localStorage fallback when window.opener is lost during OAuth
- Add PWA manifest with share_target configuration
- Add minimal service worker for PWA requirements
- Add share-target route handler for receiving shared URLs
Fixes #4
Features:
- Automatically send bookmarks to Instapaper when tagged with reading list tag
- Encrypted credential storage using AES-GCM encryption
- Credential validation before saving (tests Instapaper API connection)
- Settings UI with toggle, username/password fields
- Enhanced success feedback with checkmark and verification message
- Silent failure pattern: bookmark updates succeed even if Instapaper fails
Security:
- Separate ENCRYPTION_KEY environment variable for credential encryption
- AES-GCM with 256-bit key derived via PBKDF2 (100,000 iterations)
- Passwords never logged or returned in API responses
- Credentials only decrypted when actively needed
OAuth improvements:
- Fix BASE_URL auto-detection to properly handle ngrok HTTPS forwarding
- Check X-Forwarded-Proto header for correct protocol detection
- Support dynamic ngrok URLs without manual configuration
Database:
- Migration 002: Add instapaper_enabled, instapaper_username_encrypted,
instapaper_password_encrypted columns to user_settings
Testing:
- All 73 tests passing
- Unit tests for encryption round-trip
- Unit tests for Instapaper API client with mocked fetch
- Integration tests for settings API with credential validation
- Add navigation tabs in header (Bookmarks / Reading List)
- Reading List shows bookmarks filtered by a configurable tag
- Add Settings page to configure reading list tag (default: toread)
- Store user settings in Turso database with migration
- Reading List view has full-width teaser cards and tag filtering
- Add Settings link to user menu dropdown
- Add GET/PATCH /api/settings endpoints
- Include settings in initial-data response
- Remove debug endpoint that exposed session information
- Add SSRF protection to URL enrichment (blocks private IPs, localhost, cloud metadata)
- Add output sanitization (length limits, control char removal, favicon URL validation)
- Add security headers middleware (X-Frame-Options, HSTS, etc.)
- Add 20 security tests covering all new protections
Add tests that exercise the actual feature flows using fakes
for all external dependencies (OAuth, PDS, URL fetching).
New test files:
- tests/test-helpers.ts: Mock utilities for sessions and fetch
- tests/bookmarks.test.ts: 10 tests for bookmark CRUD
- tests/enrichment.test.ts: 11 tests for URL metadata extraction
- tests/plc-resolver.test.ts: 7 tests for DID resolution
Modified lib/session.ts to add setTestSessionProvider() hook
for injecting mock sessions in tests.
Total: 35 tests, all running without network calls.
The KV caching was causing 'Deno.openKv is not a function' errors
in production. Since the app is already fast without caching,
removing it entirely simplifies the code and eliminates the errors.
- Remove kv-cache.ts utility module
- Simplify plc-resolver.ts to call PLC directory directly
- Simplify enrichment.ts to fetch metadata directly
- Remove plc-resolver.test.ts (only tested caching behavior)
Tests verify:
- getCached returns cached values when not expired
- getCached caches null values (documenting the problem)
- invalidateCache removes cached values
- PLC resolver recovers from cached null by re-fetching
- PLC resolver does not loop infinitely on persistent null
Added --unstable-kv flag to test task for Deno KV support.
- lib/kv-cache.ts: Generic cache-aside pattern using Deno KV
- lib/plc-resolver.ts: Cached DID document resolution (1h TTL)
- lib/enrichment.ts: Cached URL metadata extraction (24h TTL)
- routes/api/share.ts: Use cached PLC resolver
- routes/share/rss.ts: Use cached PLC resolver
Reduces PLC directory load and speeds up share page requests.
Split main.ts (1,417 lines) into modular route files:
- routes/api/bookmarks.ts - Bookmark CRUD operations
- routes/api/tags.ts - Tag CRUD operations
- routes/api/auth.ts - Session management
- routes/api/initial-data.ts - Combined data fetch
- routes/api/share.ts - Public bookmark sharing
- routes/oauth.ts - OAuth login flow
- routes/share/rss.ts - RSS feed generation
- routes/static.ts - Static files and SPA routing
- lib/route-utils.ts - Shared utilities
main.ts is now just an orchestrator (79 lines).
All files under 500 lines per project guidelines.
- Build script now outputs bundle.[hash].js with 8-char content hash
- Creates manifest.json for runtime bundle path lookup
- Automatically cleans up old bundle files on rebuild
- Cache headers: immutable (1yr) for hashed bundles, 1hr for other assets
- HTML dynamically injects correct hashed bundle path
- Add all dependencies to deno.json imports with versions
- Update all files to use bare specifiers instead of inline URLs
- Remove no-import-prefix and no-unversioned-import lint exclusions
- Exclude static/ directory from formatting (bundled files)
- Update CLAUDE.md and README.md with accurate project structure
- Add build:frontend task using npm:esbuild to create bundled JS
- Upgrade all React imports from esm.sh/react to esm.sh/react@19
- Update index.html to load /static/bundle.js instead of .tsx
- Remove esbuild-wasm runtime transpilation from file-server.ts
- Add static/bundle.js to .gitignore (build artifact)
- Update deno.json with React 19 import map and build task
This fixes the frontend not loading on Deno Deploy by pre-building
the bundle at build time instead of relying on runtime transpilation.
The file server was using getProjectRoot() which looked for 'kipclip-appview'
in the file path, but on Deno Deploy paths use 'app:///' protocol.
Changed to use resolveProjectPath() which handles both:
- Local development (file://) - uses fromFileUrl to get filesystem path
- Deno Deploy (app://) - uses URL pathname directly
- OAuth initialization is now lazy (deferred until first request)
- BASE_URL can be derived from request URL if not set in environment
- Client metadata endpoint returns dynamic URLs based on derived base
- Enables OAuth to work on Deno Deploy preview/branch URLs
Files changed:
- lib/oauth-config.ts: Add initOAuth(), getOAuth(), getBaseUrl() functions
- lib/session.ts: Use getOAuth() instead of oauth import
- main.ts: Add OAuth init middleware, update routes to use getOAuth()
- tests/api.test.ts: Initialize OAuth before creating handler
- deno.json: Remove BASE_URL from test tasks
- Update dev.ts to use standard Fresh Builder pattern:
- Use builder.listen(() => import('./main.ts')) callback
- Use builder.build() without app argument
- Add 'start' task for production: deno serve -A _fresh/server.js
- Add exclude pattern for _fresh directory in lint/format
- Update dev task with watch directories
Replace @libsql/client/web with a minimal fetch-based HTTP client for
remote Turso connections. This avoids npm dependencies (node-fetch,
@types/node) that were causing issues with Deno Deploy builds.
- Add turso-http.ts: pure fetch implementation of Turso v2 pipeline API
- Update db.ts: use HTTP client for remote, native client for local
- Local development still uses @libsql/client for file:// databases
- Replace Val Town SQLite with Turso/libSQL for database
- Use web client for remote Turso, native client for local dev
- Add Sentry for error tracking, replacing email alerts
- Remove local-sqlite.ts (no longer needed)
- Update .env.example with Turso and Sentry vars
- Add COOKIE_SECRET to test tasks in deno.json