Compare commits
6 Commits
abb80c05d8
...
cd58b1c1cd
| Author | SHA1 | Date |
|---|---|---|
|
|
cd58b1c1cd | |
|
|
01b5a84e42 | |
|
|
478c1f6774 | |
|
|
420ad28d9a | |
|
|
b1c3ceeab7 | |
|
|
a5c5c7f441 |
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
id: task-015
|
||||
title: Set up Cloudflare D1 email-collector database for cross-site subscriptions
|
||||
status: To Do
|
||||
assignee: []
|
||||
created_date: '2025-12-04 12:00'
|
||||
labels:
|
||||
- infrastructure
|
||||
- cloudflare
|
||||
- d1
|
||||
- email
|
||||
- cross-site
|
||||
dependencies: []
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Create a standalone Cloudflare D1 database for collecting email subscriptions across all websites (mycofi.earth, canvas.jeffemmett.com, decolonizeti.me, etc.) with easy export capabilities.
|
||||
|
||||
**Purpose:**
|
||||
- Unified email collection from all sites
|
||||
- Page-separated lists (e.g., /newsletter, /waitlist, /landing)
|
||||
- Simple CSV/JSON export for email campaigns
|
||||
- GDPR-compliant with unsubscribe tracking
|
||||
|
||||
**Sites to integrate:**
|
||||
- mycofi.earth
|
||||
- canvas.jeffemmett.com
|
||||
- decolonizeti.me
|
||||
- games.jeffemmett.com
|
||||
- Future sites
|
||||
|
||||
**Key Features:**
|
||||
- Double opt-in verification
|
||||
- Source tracking (which site, which page)
|
||||
- Export in multiple formats (CSV, JSON, Mailchimp)
|
||||
- Basic admin dashboard or CLI for exports
|
||||
- Rate limiting to prevent abuse
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 D1 database 'email-collector' created on Cloudflare
|
||||
- [ ] #2 Schema deployed with subscribers, verification_tokens tables
|
||||
- [ ] #3 POST /api/subscribe endpoint accepts email + source_site + source_page
|
||||
- [ ] #4 Email verification flow with token-based double opt-in
|
||||
- [ ] #5 GET /api/emails/export returns CSV with filters (site, date, verified)
|
||||
- [ ] #6 Unsubscribe endpoint and tracking
|
||||
- [ ] #7 Rate limiting prevents spam submissions
|
||||
- [ ] #8 At least one site integrated and collecting emails
|
||||
<!-- AC:END -->
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
id: task-016
|
||||
title: Add encryption for CryptID emails at rest
|
||||
status: To Do
|
||||
assignee: []
|
||||
created_date: '2025-12-04 12:01'
|
||||
labels:
|
||||
- security
|
||||
- cryptid
|
||||
- encryption
|
||||
- privacy
|
||||
- d1
|
||||
dependencies:
|
||||
- task-017
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Enhance CryptID security by encrypting email addresses stored in D1 database. This protects user privacy even if the database is compromised.
|
||||
|
||||
**Encryption Strategy:**
|
||||
- Encrypt email addresses before storing in D1
|
||||
- Use Cloudflare Workers KV or environment secret for encryption key
|
||||
- Store encrypted email + hash for lookups
|
||||
- Decrypt only when needed (sending emails, display)
|
||||
|
||||
**Implementation Options:**
|
||||
1. **AES-GCM encryption** with key in Worker secret
|
||||
2. **Deterministic encryption** for email lookups (hash-based)
|
||||
3. **Hybrid approach**: Hash for lookup index, AES for actual email
|
||||
|
||||
**Schema Changes:**
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN email_encrypted TEXT;
|
||||
ALTER TABLE users ADD COLUMN email_hash TEXT; -- For lookups
|
||||
-- Migrate existing emails, then drop plaintext column
|
||||
```
|
||||
|
||||
**Considerations:**
|
||||
- Key rotation strategy
|
||||
- Performance impact on lookups
|
||||
- Backup/recovery implications
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Encryption key securely stored in Worker secrets
|
||||
- [ ] #2 Emails encrypted before D1 insert
|
||||
- [ ] #3 Email lookup works via hash index
|
||||
- [ ] #4 Decryption works for email display and sending
|
||||
- [ ] #5 Existing emails migrated to encrypted format
|
||||
- [ ] #6 Key rotation procedure documented
|
||||
- [ ] #7 No plaintext emails in database
|
||||
<!-- AC:END -->
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
id: task-016
|
||||
title: Configure CryptID secrets and environment variables
|
||||
status: To Do
|
||||
assignee: []
|
||||
created_date: '2025-12-04 12:00'
|
||||
labels:
|
||||
- infrastructure
|
||||
- cloudflare
|
||||
- cryptid
|
||||
- secrets
|
||||
- email
|
||||
dependencies:
|
||||
- task-015
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Set up the required secrets and environment variables for CryptID email functionality on Cloudflare Workers.
|
||||
|
||||
**Required Secrets:**
|
||||
- SENDGRID_API_KEY - For sending verification emails
|
||||
- CRYPTID_EMAIL_FROM - Sender email address (e.g., auth@jeffemmett.com)
|
||||
- APP_URL - Base URL for verification links (e.g., https://canvas.jeffemmett.com)
|
||||
|
||||
**Configuration:**
|
||||
- Secrets set for both production and dev environments
|
||||
- SendGrid account configured with verified sender domain
|
||||
- Email templates tested
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 SENDGRID_API_KEY secret set via wrangler secret put
|
||||
- [ ] #2 CRYPTID_EMAIL_FROM secret configured
|
||||
- [ ] #3 APP_URL environment variable set in wrangler.toml
|
||||
- [ ] #4 SendGrid sender domain verified (jeffemmett.com or subdomain)
|
||||
- [ ] #5 Test email sends successfully from Worker
|
||||
<!-- AC:END -->
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
id: task-016
|
||||
title: Create Cloudflare D1 cryptid-auth database
|
||||
status: To Do
|
||||
assignee: []
|
||||
created_date: '2025-12-04 12:00'
|
||||
labels:
|
||||
- infrastructure
|
||||
- cloudflare
|
||||
- d1
|
||||
- cryptid
|
||||
- auth
|
||||
- security
|
||||
dependencies: []
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Create the D1 database on Cloudflare for CryptID authentication system. This is the first step before deploying the email recovery feature.
|
||||
|
||||
**Database Purpose:**
|
||||
- Store user accounts linked to CryptID usernames
|
||||
- Store device public keys for multi-device auth
|
||||
- Store verification tokens for email/device linking
|
||||
- Enable account recovery via verified email
|
||||
|
||||
**Security Considerations:**
|
||||
- Emails should be encrypted at rest (Phase 2)
|
||||
- Public keys are safe to store (not secrets)
|
||||
- Tokens are time-limited and single-use
|
||||
- No passwords stored (WebCrypto key-based auth)
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 D1 database 'cryptid-auth' created via wrangler d1 create
|
||||
- [ ] #2 D1 database 'cryptid-auth-dev' created for dev environment
|
||||
- [ ] #3 Database IDs added to wrangler.toml (replacing placeholders)
|
||||
- [ ] #4 Schema from worker/schema.sql deployed to both databases
|
||||
- [ ] #5 Verified tables exist: users, device_keys, verification_tokens
|
||||
<!-- AC:END -->
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
id: task-017
|
||||
title: Deploy CryptID email recovery to dev branch and test
|
||||
status: To Do
|
||||
assignee: []
|
||||
created_date: '2025-12-04 12:00'
|
||||
labels:
|
||||
- feature
|
||||
- cryptid
|
||||
- auth
|
||||
- testing
|
||||
- dev-branch
|
||||
dependencies:
|
||||
- task-015
|
||||
- task-016
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Push the existing CryptID email recovery code changes to dev branch and test the full flow before merging to main.
|
||||
|
||||
**Code Changes Ready:**
|
||||
- src/App.tsx - Routes for /verify-email, /link-device
|
||||
- src/components/auth/CryptID.tsx - Email linking flow
|
||||
- src/components/auth/Profile.tsx - Email management UI, device list
|
||||
- src/css/crypto-auth.css - Styling for email/device modals
|
||||
- worker/types.ts - Updated D1 types
|
||||
- worker/worker.ts - Auth API routes
|
||||
- worker/cryptidAuth.ts - Auth handlers (already committed)
|
||||
|
||||
**Test Scenarios:**
|
||||
1. Link email to existing CryptID account
|
||||
2. Verify email via link
|
||||
3. Request device link from new device
|
||||
4. Approve device link via email
|
||||
5. View and revoke linked devices
|
||||
6. Recover account on new device via email
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 All CryptID changes committed to dev branch
|
||||
- [ ] #2 Worker deployed to dev environment
|
||||
- [ ] #3 Link email flow works end-to-end
|
||||
- [ ] #4 Email verification completes successfully
|
||||
- [ ] #5 Device linking via email works
|
||||
- [ ] #6 Device revocation works
|
||||
- [ ] #7 Profile shows linked email and devices
|
||||
- [ ] #8 No console errors in happy path
|
||||
<!-- AC:END -->
|
||||
|
|
@ -11,39 +11,23 @@ function isValidIndexKey(index: string): boolean {
|
|||
return false
|
||||
}
|
||||
|
||||
// The first character indicates the integer part length:
|
||||
// 'a' = 1 digit, 'b' = 2 digits, etc. for positive integers
|
||||
// 'Z' = 1 digit, 'Y' = 2 digits, etc. for negative integers
|
||||
// But for normal shapes, 'a' followed by a digit is the most common pattern
|
||||
// tldraw uses fractional indexing where:
|
||||
// - First character is a lowercase letter indicating integer part length (a=1, b=2, c=3, etc.)
|
||||
// - Followed by alphanumeric characters for the value and optional jitter
|
||||
// Examples: "a0", "a1", "b10", "b99", "c100", "a1V4rr", "b10Lz"
|
||||
//
|
||||
// Also uppercase letters for negative indices (Z=1, Y=2, etc.)
|
||||
|
||||
// Simple invalid patterns that are definitely wrong:
|
||||
// - Just a number like "1", "2"
|
||||
// - Old format like "b1", "c1" (letter + single digit that's not a valid fractional index)
|
||||
// - Empty or whitespace
|
||||
|
||||
// Valid fractional indices from tldraw start with 'a' for small positive numbers
|
||||
// and follow with digits + optional alphanumeric jitter
|
||||
// Pattern: starts with 'a', followed by at least one digit, then optional alphanumeric chars
|
||||
|
||||
// Simple patterns that are DEFINITELY invalid for tldraw:
|
||||
// "b1", "c1", "d1" etc - these are old non-fractional indices
|
||||
if (/^[b-z]\d$/i.test(index)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Valid tldraw indices should start with lowercase 'a' followed by digits
|
||||
// and optionally more alphanumeric characters for the fractional part
|
||||
// Examples from actual tldraw: "a0", "a1", "a24sT", "a1V4rr"
|
||||
if (/^a\d/.test(index)) {
|
||||
// Valid fractional index: lowercase letter followed by alphanumeric characters
|
||||
if (/^[a-z][a-zA-Z0-9]+$/.test(index)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Also allow 'Z' prefix for very high indices (though rare)
|
||||
if (/^Z[a-z]/i.test(index)) {
|
||||
// Also allow uppercase prefix for negative/very high indices
|
||||
if (/^[A-Z][a-zA-Z0-9]+$/.test(index)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If none of the above, it's likely invalid
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,13 +23,15 @@ function minimalSanitizeRecord(record: any): any {
|
|||
// NOTE: Index assignment is handled by assignSequentialIndices() during format conversion
|
||||
// Here we only ensure index exists with a valid format, not strictly validate
|
||||
// This preserves layer order that was established during conversion
|
||||
// Valid formats: a1, a2, a10, a1V, a1Lz, etc. (fractional indexing)
|
||||
// tldraw uses fractional indexing: a0, a1, b10, c100, a1V4rr, etc.
|
||||
// - First letter (a-z) indicates integer part length (a=1 digit, b=2 digits, etc.)
|
||||
// - Uppercase (A-Z) for negative/special indices
|
||||
if (!sanitized.index || typeof sanitized.index !== 'string' || sanitized.index.length === 0) {
|
||||
// Only assign default if truly missing
|
||||
sanitized.index = 'a1'
|
||||
} else if (!/^a\d/.test(sanitized.index) && !/^Z[a-z]/i.test(sanitized.index)) {
|
||||
// Accept any index starting with 'a' + digit, or 'Z' prefix
|
||||
// Only reset clearly invalid formats
|
||||
} else if (!/^[a-zA-Z][a-zA-Z0-9]+$/.test(sanitized.index)) {
|
||||
// Accept any letter followed by alphanumeric characters
|
||||
// Only reset clearly invalid formats (e.g., numbers, empty, single char)
|
||||
console.warn(`⚠️ MinimalSanitization: Invalid index format "${sanitized.index}" for shape ${sanitized.id}`)
|
||||
sanitized.index = 'a1'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,31 +20,23 @@ import { getDocumentId, saveDocumentId } from "./documentIdMapping"
|
|||
function isValidTldrawIndex(index: string): boolean {
|
||||
if (!index || typeof index !== 'string' || index.length === 0) return false
|
||||
|
||||
// The first character indicates the integer part length:
|
||||
// 'a' = 1 digit, 'b' = 2 digits, etc. for positive integers
|
||||
// 'Z' = 1 digit, 'Y' = 2 digits, etc. for negative integers
|
||||
// But for normal shapes, 'a' followed by a digit is the most common pattern
|
||||
// tldraw uses fractional indexing where:
|
||||
// - First character is a lowercase letter indicating integer part length (a=1, b=2, c=3, etc.)
|
||||
// - Followed by alphanumeric characters for the value and optional jitter
|
||||
// Examples: "a0", "a1", "b10", "b99", "c100", "a1V4rr", "b10Lz"
|
||||
//
|
||||
// Also uppercase letters for negative indices (Z=1, Y=2, etc.)
|
||||
|
||||
// Simple patterns that are DEFINITELY invalid for tldraw:
|
||||
// "b1", "c1", "d1" etc - these are old non-fractional indices (single letter + single digit)
|
||||
// These were used before tldraw switched to fractional indexing
|
||||
if (/^[b-z]\d$/i.test(index)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Valid tldraw indices should start with lowercase 'a' followed by digits
|
||||
// and optionally more alphanumeric characters for the fractional/jitter part
|
||||
// Examples from actual tldraw: "a0", "a1", "a24sT", "a1V4rr"
|
||||
if (/^a\d/.test(index)) {
|
||||
// Valid fractional index: lowercase letter followed by alphanumeric characters
|
||||
if (/^[a-z][a-zA-Z0-9]+$/.test(index)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Also allow 'Z' prefix for very high indices (though rare)
|
||||
if (/^Z[a-z]/i.test(index)) {
|
||||
// Also allow uppercase prefix for negative/very high indices
|
||||
if (/^[A-Z][a-zA-Z0-9]+$/.test(index)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If none of the above, it's likely invalid
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68,20 +68,18 @@ import "@/css/style.css"
|
|||
import "@/css/obsidian-browser.css"
|
||||
|
||||
// Helper to validate and fix tldraw IndexKey format
|
||||
// Valid: "a0", "a1", "a24sT", "a1V4rr" - Invalid: "b1", "c1" (old format)
|
||||
// tldraw uses fractional indexing: a0, a1, b10, c100, a1V4rr, etc.
|
||||
// - First letter (a-z) indicates integer part length (a=1 digit, b=2 digits, etc.)
|
||||
// - Uppercase (A-Z) for negative/special indices
|
||||
function sanitizeIndex(index: any): IndexKey {
|
||||
if (!index || typeof index !== 'string' || index.length === 0) {
|
||||
return 'a1' as IndexKey
|
||||
}
|
||||
// Old format "b1", "c1" etc are invalid (single letter + single digit)
|
||||
if (/^[b-z]\d$/i.test(index)) {
|
||||
return 'a1' as IndexKey
|
||||
}
|
||||
// Valid: starts with 'a' followed by at least one digit
|
||||
if (/^a\d/.test(index)) {
|
||||
// Valid: letter followed by alphanumeric characters
|
||||
if (/^[a-zA-Z][a-zA-Z0-9]+$/.test(index)) {
|
||||
return index as IndexKey
|
||||
}
|
||||
// Fallback
|
||||
// Fallback for invalid formats
|
||||
return 'a1' as IndexKey
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -747,11 +747,12 @@ export class AutomergeDurableObject {
|
|||
shapesNeedingIndex.sort((a, b) => a.originalIndex - b.originalIndex)
|
||||
|
||||
// Check if shapes already have valid indices we should preserve
|
||||
// Valid index: starts with 'a' followed by digits, optionally followed by alphanumeric jitter
|
||||
// Valid tldraw fractional index: starts with a lowercase letter followed by alphanumeric characters
|
||||
// Examples: a1, a2, b1, c10, a1V, a1Lz, etc. (the letter increments as indices grow)
|
||||
const isValidIndex = (idx: any): boolean => {
|
||||
if (!idx || typeof idx !== 'string' || idx.length === 0) return false
|
||||
// Valid fractional index format: a1, a2, a1V, a10, a1Lz, etc.
|
||||
if (/^a\d/.test(idx)) return true
|
||||
// Valid fractional index format: lowercase letter followed by alphanumeric (a1, b1, c10, a1V, etc.)
|
||||
if (/^[a-z][a-zA-Z0-9]+$/.test(idx)) return true
|
||||
// Also allow 'Z' prefix for very high indices
|
||||
if (/^Z[a-z]/i.test(idx)) return true
|
||||
return false
|
||||
|
|
|
|||
Loading…
Reference in New Issue