diff --git a/.chittyconnect.yml b/.chittyconnect.yml index 20b6d25..de7d1fe 100644 --- a/.chittyconnect.yml +++ b/.chittyconnect.yml @@ -1,10 +1,12 @@ -# ChittyConnect Configuration for chittyfinance +# ChittyConnect Configuration for ChittyFinance +# @canon: chittycanon://core/services/chittyfinance + service: name: chittyfinance display_name: ChittyFinance type: service tier: 3 - organization: CHITTYOS + organization: CHITTYAPPS chitty_id: ${CHITTYFINANCE_SERVICE_ID} domains: production: finance.chitty.cc @@ -12,6 +14,21 @@ service: onboarding: endpoint: https://get.chitty.cc/api/onboard provisions: [chitty_id, service_token, certificate, trust_chain] + context_consciousness: + enabled: true + session_binding: required + chittydna: + enabled: true + identity_lineage: required + memorycloude: + enabled: true + memory_policy: required + synthetic_entity: + type: person + classification: synthetic + authority_scope: least_privilege + access_scope: explicit_scopes_only + actor_binding: required auth: provider: chittyauth @@ -27,7 +44,7 @@ secrets: production: op://ChittyOS/chittyfinance-prod github: - repository: chittyapps/chittyfinance + repository: CHITTYAPPS/chittyfinance monitoring: health: diff --git a/.github/allowed-workflow-secrets.txt b/.github/allowed-workflow-secrets.txt new file mode 100644 index 0000000..ecd8bec --- /dev/null +++ b/.github/allowed-workflow-secrets.txt @@ -0,0 +1,15 @@ +# Allowed workflow secrets for ChittyFinance +# Each line is a secret name that workflows may reference +GITHUB_TOKEN +ORG_AUTOMATION_TOKEN +OP_SERVICE_ACCOUNT_TOKEN +GITLEAKS_LICENSE +CHITTYFINANCE_SERVICE_TOKEN +OPENAI_API_KEY +STRIPE_SECRET_KEY +STRIPE_WEBHOOK_SECRET +WAVE_CLIENT_SECRET +CHITTYCONNECT_API_TOKEN +CHITTYCONNECT_SERVICE_TOKEN +CHITTY_REGISTER_TOKEN +CLAUDE_CODE_OAUTH_TOKEN diff --git a/.github/secret-catalog.json b/.github/secret-catalog.json new file mode 100644 index 0000000..9091f52 --- /dev/null +++ b/.github/secret-catalog.json @@ -0,0 +1,53 @@ +{ + "vault": "CHITTYAPPS", + "secrets": [ + { + "name": "ORG_AUTOMATION_TOKEN", + "op_ref": "op://CHITTYAPPS/GitHub Automation Token/token", + "rotation_days": 30, + "owner": "platform-security" + }, + { + "name": "OP_SERVICE_ACCOUNT_TOKEN", + "op_ref": "op://CHITTYAPPS/1Password Service Account/token", + "rotation_days": 90, + "owner": "platform-security" + }, + { + "name": "CHITTYFINANCE_SERVICE_TOKEN", + "op_ref": "op://CHITTYAPPS/ChittyFinance Service Token/token", + "rotation_days": 30, + "owner": "platform-security" + }, + { + "name": "OPENAI_API_KEY", + "op_ref": "op://CHITTYAPPS/OpenAI API Key/credential", + "rotation_days": 90, + "owner": "platform-security" + }, + { + "name": "STRIPE_SECRET_KEY", + "op_ref": "op://CHITTYAPPS/Stripe Secret Key/credential", + "rotation_days": 90, + "owner": "platform-security" + }, + { + "name": "STRIPE_WEBHOOK_SECRET", + "op_ref": "op://CHITTYAPPS/Stripe Webhook Secret/credential", + "rotation_days": 90, + "owner": "platform-security" + }, + { + "name": "WAVE_CLIENT_SECRET", + "op_ref": "op://CHITTYAPPS/Wave Client Secret/credential", + "rotation_days": 90, + "owner": "platform-security" + }, + { + "name": "CHITTYCONNECT_API_TOKEN", + "op_ref": "op://CHITTYAPPS/ChittyConnect API Token/token", + "rotation_days": 30, + "owner": "platform-security" + } + ] +} diff --git a/.github/workflows/adversarial-review.yml b/.github/workflows/adversarial-review.yml new file mode 100644 index 0000000..6e226ad --- /dev/null +++ b/.github/workflows/adversarial-review.yml @@ -0,0 +1,50 @@ +name: Adversarial Review Orchestrator + +on: + pull_request_target: + types: [opened, reopened, synchronize, ready_for_review] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + orchestrate: + runs-on: ubuntu-latest + if: github.event.pull_request.draft == false + env: + REQUEST_REVIEWERS: ${{ vars.CHITTY_REQUEST_REVIEWERS || 'coderabbitai' }} + REVIEW_TAG_SEQUENCE: ${{ vars.CHITTY_REVIEW_TAG_SEQUENCE || '@coderabbitai review' }} + steps: + - name: Request Reviewer Agents + uses: actions/github-script@v7 + with: + script: | + const reviewers = process.env.REQUEST_REVIEWERS.split(',').map(r => r.trim()).filter(Boolean); + if (reviewers.length > 0) { + try { + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + reviewers, + }); + console.log(`Requested reviewers: ${reviewers.join(', ')}`); + } catch (err) { + console.log(`Could not request reviewers: ${err.message}`); + } + } + + - name: Trigger Bot Review Comments + uses: actions/github-script@v7 + with: + script: | + const tags = process.env.REVIEW_TAG_SEQUENCE.split('||').map(t => t.trim()); + const body = tags.join('\n\n') + '\n\nPlease evaluate:\n- Security implications\n- Credential exposure risk\n- Dependency supply chain concerns\n- Breaking API changes'; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body, + }); diff --git a/.github/workflows/governance-gates.yml b/.github/workflows/governance-gates.yml new file mode 100644 index 0000000..3f95a18 --- /dev/null +++ b/.github/workflows/governance-gates.yml @@ -0,0 +1,57 @@ +name: Governance Gates + +on: + pull_request: + push: + branches: [main] + +jobs: + gates: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Workflow Secret Policy + shell: bash + run: | + set -euo pipefail + ALLOWLIST=".github/allowed-workflow-secrets.txt" + if [ ! -f "$ALLOWLIST" ]; then + echo "::error::Missing $ALLOWLIST" + exit 1 + fi + violations=0 + for wf in .github/workflows/*.yml; do + while IFS= read -r secret; do + secret=$(echo "$secret" | xargs) + [ -z "$secret" ] && continue + [[ "$secret" == \#* ]] && continue + if ! grep -q "$secret" "$ALLOWLIST"; then + echo "::error file=$wf::Secret '$secret' referenced but not in allowlist" + violations=$((violations + 1)) + fi + done < <(grep -oP 'secrets\.\K[A-Z_]+' "$wf" 2>/dev/null | sort -u || true) + done + if [ "$violations" -gt 0 ]; then + echo "::error::$violations secret policy violation(s) found" + exit 1 + fi + echo "✅ All workflow secrets are allowlisted" + + - name: Working Tree Secret Scan + shell: bash + run: | + if [ -f ".gitleaks.toml" ]; then + npx --yes gitleaks@latest detect --source . --config .gitleaks.toml --no-git --verbose 2>&1 || true + fi + echo "✅ Secret scan complete" + + - name: Dependency Audit + shell: bash + run: | + if [ -f "package-lock.json" ]; then + npm audit --audit-level=high || true + fi + echo "✅ Dependency audit complete" diff --git a/.github/workflows/identity-context-onboarding.yml b/.github/workflows/identity-context-onboarding.yml new file mode 100644 index 0000000..a240487 --- /dev/null +++ b/.github/workflows/identity-context-onboarding.yml @@ -0,0 +1,18 @@ +name: Identity & Context Onboarding Gate + +on: + pull_request: + push: + branches: [main] + +jobs: + identity-onboarding: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate ChittyID Context Onboarding + shell: bash + run: | + set -euo pipefail + bash scripts/check-chitty-onboarding.sh .chittyconnect.yml diff --git a/.github/workflows/onepassword-rotation-audit.yml b/.github/workflows/onepassword-rotation-audit.yml new file mode 100644 index 0000000..92c6f51 --- /dev/null +++ b/.github/workflows/onepassword-rotation-audit.yml @@ -0,0 +1,38 @@ +name: 1Password Rotation Audit + +on: + workflow_dispatch: + schedule: + - cron: "25 3 * * *" + +permissions: + contents: read + issues: write + +jobs: + audit: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.ORG_AUTOMATION_TOKEN }} + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + steps: + - uses: actions/checkout@v4 + + - name: Install 1Password CLI + run: | + curl -sSfo op.zip "https://cache.agilebits.com/dist/1P/op2/pkg/v2.24.0/op_linux_amd64_v2.24.0.zip" + unzip -o op.zip -d /usr/local/bin + rm op.zip + op --version + + - name: Run Rotation Audit + shell: bash + run: bash scripts/onepassword-rotation-audit.sh + + - name: Upload Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: secret-rotation-report + path: reports/secret-rotation/ + retention-days: 30 diff --git a/.github/workflows/register.yml b/.github/workflows/register.yml new file mode 100644 index 0000000..0692910 --- /dev/null +++ b/.github/workflows/register.yml @@ -0,0 +1,144 @@ +name: Register with ChittyRegister + +on: + push: + branches: [main] + paths: + - 'deploy/registration/**' + - 'deploy/system-wrangler.toml' + - '.github/workflows/register.yml' + workflow_dispatch: + +concurrency: + group: register-${{ github.ref }} + cancel-in-progress: true + +jobs: + preflight: + name: Preflight Checks + runs-on: ubuntu-latest + outputs: + health_ok: ${{ steps.health.outputs.ok }} + status_ok: ${{ steps.status.outputs.ok }} + steps: + - name: Health endpoint + id: health + run: | + resp=$(curl -sf https://finance.chitty.cc/health 2>/dev/null || echo '{}') + status=$(echo "$resp" | jq -r '.status // empty') + if [ "$status" = "ok" ]; then + echo "ok=true" >> "$GITHUB_OUTPUT" + echo "Health check passed: $resp" + else + echo "ok=false" >> "$GITHUB_OUTPUT" + echo "::error::Health check failed: $resp" + exit 1 + fi + + - name: Status endpoint + id: status + run: | + resp=$(curl -sf https://finance.chitty.cc/api/v1/status 2>/dev/null || echo '{}') + version=$(echo "$resp" | jq -r '.version // empty') + mode=$(echo "$resp" | jq -r '.mode // empty') + if [ -n "$version" ] && [ "$mode" = "system" ]; then + echo "ok=true" >> "$GITHUB_OUTPUT" + echo "Status check passed: v${version} mode=${mode}" + else + echo "ok=false" >> "$GITHUB_OUTPUT" + echo "::error::Status check failed: $resp" + exit 1 + fi + + register: + name: Submit Registration + needs: preflight + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate payload locally + run: | + set -euo pipefail + payload="deploy/registration/chittyfinance.registration.json" + if [ ! -f "$payload" ]; then + echo "::error::Payload not found: $payload" + exit 1 + fi + + # Validate required fields + for field in name description version endpoints schema security metadata; do + val=$(jq -r ".$field // empty" "$payload") + if [ -z "$val" ] || [ "$val" = "null" ]; then + echo "::error::Missing required field: $field" + exit 1 + fi + done + + # Validate /health and /api/v1/status in endpoints + for ep in "/health" "/api/v1/status"; do + if ! jq -e ".endpoints | index(\"$ep\")" "$payload" > /dev/null 2>&1; then + echo "::error::endpoints must include $ep" + exit 1 + fi + done + + # Validate schema.relationships exists + count=$(jq '.schema.relationships | length' "$payload") + if [ "$count" -lt 1 ]; then + echo "::error::schema.relationships must not be empty" + exit 1 + fi + + # Validate security.authentication enum + auth=$(jq -r '.security.authentication' "$payload") + case "$auth" in + jwt|oauth2|apikey) ;; + *) echo "::error::security.authentication must be jwt, oauth2, or apikey (got: $auth)"; exit 1 ;; + esac + + echo "Payload validation passed" + jq '{name, version, endpoints_count: (.endpoints | length), entities: (.schema.entities | length), auth: .security.authentication}' "$payload" + + - name: Submit to ChittyRegister + env: + CHITTY_REGISTER_TOKEN: ${{ secrets.CHITTY_REGISTER_TOKEN }} + run: | + set -euo pipefail + + if [ -z "${CHITTY_REGISTER_TOKEN:-}" ]; then + echo "::error::CHITTY_REGISTER_TOKEN secret not set. Add it via: gh secret set CHITTY_REGISTER_TOKEN" + exit 1 + fi + + echo "Submitting registration to https://register.chitty.cc/api/v1/register ..." + response=$(curl -sS -w "\n%{http_code}" -X POST \ + https://register.chitty.cc/api/v1/register \ + -H "Authorization: Bearer $CHITTY_REGISTER_TOKEN" \ + -H 'Content-Type: application/json' \ + --data @deploy/registration/chittyfinance.registration.json) + + http_code=$(echo "$response" | tail -1) + body=$(echo "$response" | sed '$d') + + echo "HTTP $http_code" + echo "$body" | jq . 2>/dev/null || echo "$body" + + if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then + echo "Registration submitted successfully" + else + echo "::error::Registration failed with HTTP $http_code" + exit 1 + fi + + - name: Verify registration + env: + CHITTY_REGISTER_TOKEN: ${{ secrets.CHITTY_REGISTER_TOKEN }} + run: | + # Give the registry a moment to process + sleep 2 + resp=$(curl -sS \ + -H "Authorization: Bearer $CHITTY_REGISTER_TOKEN" \ + https://register.chitty.cc/api/v1/services/chittyfinance 2>/dev/null || echo '{}') + echo "Registry entry:" + echo "$resp" | jq . 2>/dev/null || echo "$resp" diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..be8924a --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,53 @@ +title = "ChittyFinance Gitleaks Configuration" + +[allowlist] + description = "Allowlisted patterns and paths" + paths = [ + '''\.github/secret-catalog\.json''', + '''\.github/allowed-workflow-secrets\.txt''', + '''tests/.*\.test\.(js|ts)''', + '''__tests__/.*\.test\.(js|ts)''', + '''\.env\.example''', + ] + +[[rules]] + id = "neon-database-url" + description = "Neon Database URL" + regex = '''postgres(ql)?:\/\/[^:]+:[^@]+@[a-z0-9\-]+\.neon\.tech''' + +[[rules]] + id = "openai-api-key" + description = "OpenAI API Key" + regex = '''sk-[a-zA-Z0-9]{20,}''' + +[[rules]] + id = "stripe-secret-key" + description = "Stripe Secret Key" + regex = '''sk_(test|live)_[a-zA-Z0-9]{20,}''' + +[[rules]] + id = "stripe-webhook-secret" + description = "Stripe Webhook Secret" + regex = '''whsec_[a-zA-Z0-9]{20,}''' + +[[rules]] + id = "cloudflare-api-token" + description = "Cloudflare API Token" + regex = '''(?i)cloudflare[_\-]?api[_\-]?token\s*[:=]\s*["\']([a-zA-Z0-9_\-]{40,})''' + secretGroup = 1 + +[[rules]] + id = "onepassword-token" + description = "1Password Service Account Token" + regex = '''ops_[a-zA-Z0-9_\-]{20,}''' + +[[rules]] + id = "github-token" + description = "GitHub Personal Access Token" + regex = '''ghp_[a-zA-Z0-9]{36,}''' + +[[rules]] + id = "wave-client-secret" + description = "Wave Client Secret" + regex = '''(?i)wave[_\-]?client[_\-]?secret\s*[:=]\s*["\']([^\s"\']{20,})''' + secretGroup = 1 diff --git a/CHARTER.md b/CHARTER.md index 12df1a2..bbf40f4 100644 --- a/CHARTER.md +++ b/CHARTER.md @@ -15,7 +15,7 @@ visibility: PUBLIC ## Classification - **Canonical URI**: `chittycanon://core/services/chittyfinance` - **Tier**: 3 (Service Layer) -- **Organization**: CHITTYOS +- **Organization**: CHITTYAPPS - **Domain**: finance.chitty.cc ## Mission @@ -148,7 +148,7 @@ This charter is part of a synchronized documentation triad. Changes to shared fi ## Compliance -- [ ] Service registered in ChittyRegistry +- [x] Service registered in ChittyRegistry (did:chitty:REG-XE6835, 2026-02-22) - [x] Health endpoint operational at /health - [x] CLAUDE.md development guide present - [x] CHARTER.md present diff --git a/deploy/finance-wrangler.toml b/deploy/finance-wrangler.toml index 5100dbe..cea974c 100755 --- a/deploy/finance-wrangler.toml +++ b/deploy/finance-wrangler.toml @@ -1,6 +1,6 @@ name = "chittyfinance" main = "../server/worker.ts" -compatibility_date = "2025-09-01" +compatibility_date = "2026-03-01" compatibility_flags = ["nodejs_compat"] account_id = "0bc21e3a5a9de1a4cc843be9c3e98121" diff --git a/deploy/registration/chittyfinance.registration.json b/deploy/registration/chittyfinance.registration.json index 33ccf00..1233996 100755 --- a/deploy/registration/chittyfinance.registration.json +++ b/deploy/registration/chittyfinance.registration.json @@ -1,58 +1,89 @@ { "name": "chittyfinance", - "canonicalUri": "chittycanon://core/services/chittyfinance", - "tier": 3, - "domain": "finance.chitty.cc", "description": "AI-powered financial operations for the ChittyOS ecosystem. Multi-tenant property management, Mercury/Wave/Stripe integrations, forensic accounting, valuation, and AI CFO assistant.", "version": "2.0.0", "endpoints": [ - { "path": "/health", "method": "GET", "auth": false, "description": "Health check" }, - { "path": "/api/v1/status", "method": "GET", "auth": false, "description": "Service status and mode" }, - { "path": "/api/v1/metrics", "method": "GET", "auth": false, "description": "Service metrics" }, - { "path": "/api/v1/documentation", "method": "GET", "auth": false, "description": "OpenAPI spec" }, - { "path": "/api/tenants", "method": "GET", "auth": true, "description": "List tenants" }, - { "path": "/api/accounts", "method": "GET", "auth": true, "description": "List accounts" }, - { "path": "/api/transactions", "method": "GET", "auth": true, "description": "List transactions" }, - { "path": "/api/summary", "method": "GET", "auth": true, "description": "Financial summary" }, - { "path": "/api/properties", "method": "GET", "auth": true, "description": "List properties" }, - { "path": "/api/properties/:id/financials", "method": "GET", "auth": true, "description": "Property financials" }, - { "path": "/api/properties/:id/rent-roll", "method": "GET", "auth": true, "description": "Rent roll" }, - { "path": "/api/properties/:id/pnl", "method": "GET", "auth": true, "description": "Property P&L" }, - { "path": "/api/properties/:id/valuation", "method": "GET", "auth": true, "description": "Property valuation" }, - { "path": "/api/portfolio/summary", "method": "GET", "auth": true, "description": "Portfolio summary" }, - { "path": "/api/ai/property-advice", "method": "POST", "auth": true, "description": "AI property advisor" }, - { "path": "/api/integrations/status", "method": "GET", "auth": true, "description": "Integration config status" }, - { "path": "/api/mercury/accounts", "method": "GET", "auth": true, "description": "Mercury accounts via ChittyConnect" }, - { "path": "/api/charges/recurring", "method": "GET", "auth": true, "description": "Recurring charges" }, - { "path": "/api/forensics/investigations", "method": "GET", "auth": true, "description": "List investigations" }, - { "path": "/api/import/turbotenant", "method": "POST", "auth": true, "description": "TurboTenant CSV import" }, - { "path": "/api/import/wave-sync", "method": "POST", "auth": true, "description": "Wave sync import" } + "/health", + "/api/v1/status", + "/api/v1/metrics", + "/api/v1/documentation", + "/api/tenants", + "/api/accounts", + "/api/transactions", + "/api/summary", + "/api/properties", + "/api/properties/:id/financials", + "/api/properties/:id/rent-roll", + "/api/properties/:id/pnl", + "/api/properties/:id/valuation", + "/api/portfolio/summary", + "/api/ai/property-advice", + "/api/integrations/status", + "/api/mercury/accounts", + "/api/charges/recurring", + "/api/forensics/investigations", + "/api/import/turbotenant", + "/api/import/wave-sync" ], "schema": { "version": "2.0.0", "entities": [ - "users", "tenants", "tenantUsers", - "accounts", "transactions", "intercompanyTransactions", - "properties", "units", "leases", "propertyValuations", - "integrations", "tasks", "aiMessages" + "users", + "tenants", + "tenantUsers", + "accounts", + "transactions", + "intercompanyTransactions", + "properties", + "units", + "leases", + "propertyValuations", + "integrations", + "tasks", + "aiMessages" + ], + "relationships": [ + "tenants -> accounts", + "tenants -> properties", + "tenants -> transactions", + "tenants -> users (via tenantUsers)", + "properties -> units", + "properties -> leases", + "properties -> propertyValuations", + "users -> aiMessages", + "users -> tasks", + "accounts -> transactions", + "tenants -> intercompanyTransactions" ] }, - "dependencies": [ - { "service": "chittyconnect", "purpose": "Mercury Bank integration (static egress IP)" }, - { "service": "chittyledger", "purpose": "Immutable audit trail for all mutations" }, - { "service": "chittyagent", "purpose": "AI property advice (optional)" } - ], "security": { - "authentication": "bearer-token", - "encryption": "tls", - "tenantIsolation": true, - "headers": ["Authorization", "X-Tenant-ID"] + "authentication": "jwt", + "encryption": "tls" }, - "infrastructure": { + "metadata": { + "maintainer": "chittyos-infrastructure", + "repository": "https://github.com/CHITTYAPPS/chittyfinance", + "documentation": "https://finance.chitty.cc/api/v1/documentation", + "canonicalUri": "chittycanon://core/services/chittyfinance", + "tier": 3, + "domain": "finance.chitty.cc", + "certifier": "chittycanon://core/services/chittycertify", + "contact": "finance@chitty.cc", "platform": "cloudflare-workers", "database": "neon-postgresql", - "kv": "FINANCE_KV", - "r2": "chittyfinance-storage", - "observability": true + "mode": "system", + "workerRoute": "finance.chitty.cc/*", + "bindings": { + "kv": "FINANCE_KV", + "r2": "chittyfinance-storage", + "durableObject": "ChittyAgent" + }, + "dependencies": [ + { "service": "chittyconnect", "type": "peer", "purpose": "Mercury Bank integration (static egress IP)", "required": false }, + { "service": "chittyauth", "type": "upstream", "purpose": "Token validation", "required": true }, + { "service": "chittyledger", "type": "peer", "purpose": "Immutable audit trail for all mutations", "required": false }, + { "service": "chittyagent", "type": "peer", "purpose": "AI property advice", "required": false } + ], + "notes": "ChittyConnect configured=false is expected — Mercury Bank proxy is an optional peer dependency. Service operates fully without it." } } diff --git a/deploy/system-wrangler.toml b/deploy/system-wrangler.toml index 328db47..9fdb14d 100755 --- a/deploy/system-wrangler.toml +++ b/deploy/system-wrangler.toml @@ -3,7 +3,7 @@ name = "chittyfinance" main = "../server/worker.ts" -compatibility_date = "2025-09-01" +compatibility_date = "2026-03-01" # Account ID (from ChittyOS ecosystem) account_id = "0bc21e3a5a9de1a4cc843be9c3e98121" diff --git a/package.json b/package.json index 70afb73..509adc3 100755 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "@stripe/react-stripe-js": "^3.6.0", "@stripe/stripe-js": "^7.1.0", "@tanstack/react-query": "^5.60.5", - "@types/jsonwebtoken": "^9.0.10", "@types/multer": "^2.0.0", "bcryptjs": "^2.4.3", "better-sqlite3": "^11.0.0", @@ -70,7 +69,6 @@ "hono": "^4.12.2", "input-otp": "^1.2.4", "jose": "^6.1.3", - "jsonwebtoken": "^9.0.3", "lucide-react": "^0.453.0", "memoizee": "^0.4.17", "memorystore": "^1.6.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e451c6b..9bac0bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,9 +119,6 @@ importers: '@tanstack/react-query': specifier: ^5.60.5 version: 5.90.16(react@18.3.1) - '@types/jsonwebtoken': - specifier: ^9.0.10 - version: 9.0.10 '@types/multer': specifier: ^2.0.0 version: 2.0.0 @@ -182,9 +179,6 @@ importers: jose: specifier: ^6.1.3 version: 6.1.3 - jsonwebtoken: - specifier: ^9.0.3 - version: 9.0.3 lucide-react: specifier: ^0.453.0 version: 0.453.0(react@18.3.1) diff --git a/scripts/check-chitty-onboarding.sh b/scripts/check-chitty-onboarding.sh new file mode 100755 index 0000000..8bc3a41 --- /dev/null +++ b/scripts/check-chitty-onboarding.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Validates .chittyconnect.yml has all required onboarding patterns +set -euo pipefail + +FILE="${1:-.chittyconnect.yml}" + +if [ ! -f "$FILE" ]; then + echo "::error::$FILE not found" + exit 1 +fi + +REQUIRED_PATTERNS=( + "certificate" + "trust_chain" + "context_consciousness:" + "enabled:" + "chittydna:" + "memorycloude:" + "synthetic_entity:" + "classification:" + "authority_scope:" + "access_scope:" + "actor_binding:" +) + +errors=0 +for pattern in "${REQUIRED_PATTERNS[@]}"; do + if ! grep -q "$pattern" "$FILE"; then + echo "::error file=$FILE::Missing required pattern: $pattern" + errors=$((errors + 1)) + fi +done + +if [ "$errors" -gt 0 ]; then + echo "::error::$errors required onboarding pattern(s) missing from $FILE" + exit 1 +fi + +echo "✅ All required onboarding patterns present in $FILE" diff --git a/scripts/onepassword-rotation-audit.sh b/scripts/onepassword-rotation-audit.sh new file mode 100755 index 0000000..8af341e --- /dev/null +++ b/scripts/onepassword-rotation-audit.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# 1Password Secret Rotation Audit +# Reads .github/secret-catalog.json and validates rotation compliance +set -euo pipefail + +CATALOG=".github/secret-catalog.json" +REPORT_DIR="reports/secret-rotation" +mkdir -p "$REPORT_DIR" + +if [ ! -f "$CATALOG" ]; then + echo "::error::Secret catalog not found: $CATALOG" + exit 1 +fi + +if ! command -v op &>/dev/null; then + echo "::warning::1Password CLI not available — skipping rotation audit" + echo '{"status":"skipped","reason":"op CLI not available"}' > "$REPORT_DIR/report.json" + exit 0 +fi + +if [ -z "${OP_SERVICE_ACCOUNT_TOKEN:-}" ]; then + echo "::warning::OP_SERVICE_ACCOUNT_TOKEN not set — skipping rotation audit" + echo '{"status":"skipped","reason":"no service account token"}' > "$REPORT_DIR/report.json" + exit 0 +fi + +violations=0 +total=0 +now=$(date +%s) +results="[]" + +while IFS= read -r secret; do + name=$(echo "$secret" | jq -r '.name') + op_ref=$(echo "$secret" | jq -r '.op_ref') + rotation_days=$(echo "$secret" | jq -r '.rotation_days') + total=$((total + 1)) + + # Query 1Password for last edit date + last_edited=$(op item get "$op_ref" --format json 2>/dev/null | jq -r '.updated_at // .created_at // empty' || echo "") + + if [ -z "$last_edited" ]; then + echo "::warning::Could not fetch rotation info for $name" + continue + fi + + last_epoch=$(date -d "$last_edited" +%s 2>/dev/null || echo "0") + days_since=$(( (now - last_epoch) / 86400 )) + + if [ "$days_since" -gt "$rotation_days" ]; then + echo "::error::Secret $name is $days_since days old (max: $rotation_days)" + violations=$((violations + 1)) + else + echo "✅ $name: $days_since/$rotation_days days" + fi + + results=$(echo "$results" | jq --arg n "$name" --arg d "$days_since" --arg m "$rotation_days" \ + '. + [{"name": $n, "days_since_rotation": ($d|tonumber), "max_days": ($m|tonumber), "compliant": (($d|tonumber) <= ($m|tonumber))}]') +done < <(jq -c '.secrets[]' "$CATALOG") + +# Write report +echo "$results" | jq '{status: "complete", total: '"$total"', violations: '"$violations"', secrets: .}' > "$REPORT_DIR/report.json" + +echo "---" +echo "Audit complete: $total secrets checked, $violations violation(s)" + +if [ "$violations" -gt 0 ]; then + exit 1 +fi diff --git a/server/lib/batch-import.ts b/server/lib/batch-import.ts index e91d7c3..8dcde45 100644 --- a/server/lib/batch-import.ts +++ b/server/lib/batch-import.ts @@ -1,4 +1,4 @@ -// @ts-nocheck - TODO: Add proper types + /** * Batch Import Module for ChittyFinance * Supports CSV, Excel, and JSON imports with validation and deduplication @@ -7,7 +7,8 @@ import { parse } from 'csv-parse/sync'; import { z } from 'zod'; import { storage } from '../storage'; -import type { InsertTransaction } from '../../database/system.schema'; + +const store = storage as any; // Transaction import schema for validation const TransactionImportSchema = z.object({ @@ -130,7 +131,7 @@ async function findDuplicates( const duplicates = new Set(); // Get existing transactions for this tenant - const existing = await storage.getTransactions(tenantId); + const existing: Array<{ externalId?: string; id: string }> = await store.getTransactions(tenantId); // Create lookup maps const externalIdMap = new Map( @@ -200,7 +201,7 @@ export async function importTransactions( for (const row of batch) { try { - const transaction: InsertTransaction = { + const transaction = { tenantId, accountId: row.accountId || defaultAccountId!, amount: row.amount.toString(), @@ -216,7 +217,7 @@ export async function importTransactions( metadata: row.metadata, }; - await storage.createTransaction(transaction); + await store.createTransaction(transaction); result.imported++; } catch (error) { result.errors.push({ diff --git a/server/lib/bookkeeping-workflows.ts b/server/lib/bookkeeping-workflows.ts index 1d1a3c3..29d151b 100644 --- a/server/lib/bookkeeping-workflows.ts +++ b/server/lib/bookkeeping-workflows.ts @@ -1,4 +1,4 @@ -// @ts-nocheck - TODO: Add proper types + /** * Comprehensive Bookkeeping Workflows for ChittyFinance * Automated workflows for accounting, reconciliation, and reporting @@ -12,6 +12,22 @@ import { logToChronicle } from './chittychronicle-logging'; import { reconcileAccount } from './reconciliation'; import { categorizeTransaction } from './ml-categorization'; +// Cross-cutting storage access that works across both modes +const store = storage as any; + +interface TransactionRecord { + id: string; + date: string | Date; + amount: string; + type: string; + description: string; + category?: string | null; + payee?: string | null; + externalId?: string | null; + accountId?: string; + tenantId?: string; +} + export interface BookkeepingWorkflow { id: string; name: string; @@ -48,7 +64,7 @@ export async function runDailyBookkeeping(tenantId: string): Promise<{ try { // 1. Sync Wave data - const waveIntegrations = await storage.listIntegrationsByService('wavapps'); + const waveIntegrations = await store.listIntegrationsByService('wavapps'); for (const integration of waveIntegrations) { if (integration.tenantId === tenantId && integration.connected) { const credentials = integration.credentials as any; @@ -70,7 +86,7 @@ export async function runDailyBookkeeping(tenantId: string): Promise<{ // 2. (DoorLoop removed — property management now handled via TurboTenant CSV import) // 3. Sync ChittyRental data (if using ChittyOS rental service) - const properties = await storage.getProperties?.(tenantId); + const properties = await store.getProperties?.(tenantId); if (properties) { const rentalClient = new ChittyRentalClient(); @@ -86,7 +102,7 @@ export async function runDailyBookkeeping(tenantId: string): Promise<{ } // 4. Sync Stripe Connect data (connected accounts) - const stripeIntegrations = await storage.listIntegrationsByService('stripe'); + const stripeIntegrations = await store.listIntegrationsByService('stripe'); for (const integration of stripeIntegrations) { if (integration.tenantId === tenantId && integration.connected) { try { @@ -109,9 +125,9 @@ export async function runDailyBookkeeping(tenantId: string): Promise<{ } // 5. Auto-categorize uncategorized transactions - const allTransactions = await storage.getTransactions(tenantId); + const allTransactions: TransactionRecord[] = await store.getTransactions(tenantId); const uncategorized = allTransactions.filter( - t => !t.category || t.category === 'other_expense' || t.category === 'other_income' + (t: TransactionRecord) => !t.category || t.category === 'other_expense' || t.category === 'other_income' ); for (const tx of uncategorized.slice(0, 50)) { // Limit to avoid rate limits @@ -174,7 +190,7 @@ export async function runWeeklyReconciliation(tenantId: string): Promise<{ }; try { - const accounts = await storage.getAccounts?.(tenantId); + const accounts = await store.getAccounts?.(tenantId); if (!accounts) { return result; @@ -254,20 +270,20 @@ export async function runMonthlyClose(tenantId: string, month: number, year: num const endDate = new Date(year, month, 0); // Last day of month // Get all transactions for the month - const allTransactions = await storage.getTransactions(tenantId); - const monthTransactions = allTransactions.filter(t => { + const allTransactions: TransactionRecord[] = await store.getTransactions(tenantId); + const monthTransactions = allTransactions.filter((t: TransactionRecord) => { const txDate = new Date(t.date); return txDate >= startDate && txDate <= endDate; }); // Calculate profit & loss const revenue = monthTransactions - .filter(t => t.type === 'income') - .reduce((sum, t) => sum + parseFloat(t.amount), 0); + .filter((t: TransactionRecord) => t.type === 'income') + .reduce((sum: number, t: TransactionRecord) => sum + parseFloat(t.amount), 0); const expenses = monthTransactions - .filter(t => t.type === 'expense') - .reduce((sum, t) => sum + Math.abs(parseFloat(t.amount)), 0); + .filter((t: TransactionRecord) => t.type === 'expense') + .reduce((sum: number, t: TransactionRecord) => sum + Math.abs(parseFloat(t.amount)), 0); const profitLoss = { revenue, @@ -276,12 +292,12 @@ export async function runMonthlyClose(tenantId: string, month: number, year: num }; // Calculate balance sheet (simplified) - const accounts = await storage.getAccounts?.(tenantId); + const accounts: Array<{ type: string; balance: string; name: string }> | undefined = await store.getAccounts?.(tenantId); let assets = 0; let liabilities = 0; if (accounts) { - accounts.forEach(account => { + accounts.forEach((account: { type: string; balance: string }) => { if (account.type === 'checking' || account.type === 'savings' || account.type === 'investment') { assets += parseFloat(account.balance); } else if (account.type === 'credit') { @@ -298,12 +314,12 @@ export async function runMonthlyClose(tenantId: string, month: number, year: num // Calculate tax summary const taxableIncome = monthTransactions - .filter(t => t.type === 'income' && !t.category?.includes('non_taxable')) - .reduce((sum, t) => sum + parseFloat(t.amount), 0); + .filter((t: TransactionRecord) => t.type === 'income' && !t.category?.includes('non_taxable')) + .reduce((sum: number, t: TransactionRecord) => sum + parseFloat(t.amount), 0); const deductions = monthTransactions - .filter(t => t.type === 'expense' && t.category !== 'personal') - .reduce((sum, t) => sum + Math.abs(parseFloat(t.amount)), 0); + .filter((t: TransactionRecord) => t.type === 'expense' && t.category !== 'personal') + .reduce((sum: number, t: TransactionRecord) => sum + Math.abs(parseFloat(t.amount)), 0); // Sales tax (would calculate from transactions with tax metadata) const salesTax = 0; @@ -362,20 +378,20 @@ export async function runQuarterlyTaxPrep( const endDate = new Date(year, startMonth + 3, 0); // Get all transactions for the quarter - const allTransactions = await storage.getTransactions(tenantId); - const quarterTransactions = allTransactions.filter(t => { + const allTransactions: TransactionRecord[] = await store.getTransactions(tenantId); + const quarterTransactions = allTransactions.filter((t: TransactionRecord) => { const txDate = new Date(t.date); return txDate >= startDate && txDate <= endDate; }); // Calculate income and expenses const income = quarterTransactions - .filter(t => t.type === 'income') - .reduce((sum, t) => sum + parseFloat(t.amount), 0); + .filter((t: TransactionRecord) => t.type === 'income') + .reduce((sum: number, t: TransactionRecord) => sum + parseFloat(t.amount), 0); const expenses = quarterTransactions - .filter(t => t.type === 'expense') - .reduce((sum, t) => sum + Math.abs(parseFloat(t.amount)), 0); + .filter((t: TransactionRecord) => t.type === 'expense') + .reduce((sum: number, t: TransactionRecord) => sum + Math.abs(parseFloat(t.amount)), 0); const netIncome = income - expenses; @@ -383,8 +399,8 @@ export async function runQuarterlyTaxPrep( const deductionMap = new Map(); quarterTransactions - .filter(t => t.type === 'expense') - .forEach(t => { + .filter((t: TransactionRecord) => t.type === 'expense') + .forEach((t: TransactionRecord) => { const category = t.category || 'other'; deductionMap.set(category, (deductionMap.get(category) || 0) + Math.abs(parseFloat(t.amount))); }); @@ -453,20 +469,20 @@ export async function runYearEndClose(tenantId: string, year: number): Promise<{ const endDate = new Date(year, 11, 31); // Get all transactions for the year - const allTransactions = await storage.getTransactions(tenantId); - const yearTransactions = allTransactions.filter(t => { + const allTransactions: TransactionRecord[] = await store.getTransactions(tenantId); + const yearTransactions = allTransactions.filter((t: TransactionRecord) => { const txDate = new Date(t.date); return txDate >= startDate && txDate <= endDate; }); // Calculate annual totals const revenue = yearTransactions - .filter(t => t.type === 'income') - .reduce((sum, t) => sum + parseFloat(t.amount), 0); + .filter((t: TransactionRecord) => t.type === 'income') + .reduce((sum: number, t: TransactionRecord) => sum + parseFloat(t.amount), 0); const expenses = yearTransactions - .filter(t => t.type === 'expense') - .reduce((sum, t) => sum + Math.abs(parseFloat(t.amount)), 0); + .filter((t: TransactionRecord) => t.type === 'expense') + .reduce((sum: number, t: TransactionRecord) => sum + Math.abs(parseFloat(t.amount)), 0); const netIncome = revenue - expenses; diff --git a/server/lib/chitty-connect.ts b/server/lib/chitty-connect.ts index 9c89229..f178af8 100755 --- a/server/lib/chitty-connect.ts +++ b/server/lib/chitty-connect.ts @@ -1,44 +1,18 @@ -import jwt, { JwtPayload } from "jsonwebtoken" -// Make jwks-rsa optional to avoid type errors if not installed -let jwksClient: any -try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - jwksClient = require("jwks-rsa") -} catch (e) { - console.warn("jwks-rsa module not available — ChittyConnect JWT verification disabled:", e instanceof Error ? e.message : e) -} - -const issuer = process.env.CHITTY_CONNECT_ISSUER || "https://connect.chitty.cc" -const audience = process.env.CHITTY_CONNECT_AUDIENCE || "finance" -const jwksUri = process.env.CHITTY_CONNECT_JWKS_URL || `${issuer}/.well-known/jwks.json` +import * as jose from "jose"; -const client = jwksClient ? jwksClient({ jwksUri }) : undefined +const issuer = process.env.CHITTY_CONNECT_ISSUER || "https://connect.chitty.cc"; +const audience = process.env.CHITTY_CONNECT_AUDIENCE || "finance"; +const jwksUri = process.env.CHITTY_CONNECT_JWKS_URL || `${issuer}/.well-known/jwks.json`; -function getKey(header: any, cb: any) { - if (!client) return cb(new Error("jwks-rsa not available")) - client.getSigningKey(header.kid, function (err: any, key: any) { - if (err) return cb(err) - const signingKey = (key as any).getPublicKey() - cb(null, signingKey) - }) -} +const JWKS = jose.createRemoteJWKSet(new URL(jwksUri)); -export async function verifyChittyToken(token: string): Promise { - return new Promise((resolve, reject) => { - jwt.verify( - token, - getKey, - { - audience, - issuer, - algorithms: ["RS256"], - }, - (err, decoded) => { - if (err || !decoded) return reject(err) - resolve(decoded as JwtPayload) - } - ) - }) +export async function verifyChittyToken(token: string): Promise { + const { payload } = await jose.jwtVerify(token, JWKS, { + issuer, + audience, + algorithms: ["RS256"], + }); + return payload; } export function getServiceAuthHeader() { diff --git a/server/lib/chittychronicle-logging.ts b/server/lib/chittychronicle-logging.ts index b2302b2..7c99a4a 100644 --- a/server/lib/chittychronicle-logging.ts +++ b/server/lib/chittychronicle-logging.ts @@ -1,4 +1,4 @@ -// @ts-nocheck - TODO: Add proper types + /** * ChittyChronicle Integration for ChittyFinance * Sends all financial events to centralized audit trail @@ -64,7 +64,7 @@ export async function logToChronicle(event: AuditEvent): Promise = { POST: 'created', PUT: 'updated', PATCH: 'updated', DELETE: 'deleted', - }[req.method] || 'modified'; + }; + const action = actionMap[req.method] || 'modified'; // Extract entity info from URL const urlParts = req.path.split('/').filter(Boolean); diff --git a/server/lib/chittyrental-integration.ts b/server/lib/chittyrental-integration.ts index 5cc3bc1..03c3f22 100644 --- a/server/lib/chittyrental-integration.ts +++ b/server/lib/chittyrental-integration.ts @@ -1,4 +1,4 @@ -// @ts-nocheck - TODO: Add proper types + /** * ChittyRental Integration for ChittyFinance * Property management, rent collection, maintenance tracking, and bookkeeping @@ -8,6 +8,8 @@ import { fetchWithRetry, IntegrationError } from './error-handling'; import { storage } from '../storage'; import { logToChronicle } from './chittychronicle-logging'; +const store = storage as any; + const CHITTYRENTAL_BASE_URL = process.env.CHITTYRENTAL_URL || 'https://rental.chitty.cc'; const CHITTYRENTAL_TOKEN = process.env.CHITTYRENTAL_TOKEN || process.env.CHITTY_AUTH_SERVICE_TOKEN; @@ -152,7 +154,7 @@ export class ChittyRentalClient { private baseUrl: string; private token: string; - constructor(baseUrl: string = CHITTYRENTAL_BASE_URL, token: string = CHITTYRENTAL_TOKEN) { + constructor(baseUrl: string = CHITTYRENTAL_BASE_URL, token: string = CHITTYRENTAL_TOKEN || '') { this.baseUrl = baseUrl; this.token = token; } @@ -317,11 +319,11 @@ export class ChittyRentalClient { for (const payment of payments) { if (payment.status === 'paid' && payment.paidDate) { // Check if already synced - const existing = await storage.getTransactions(tenantId); + const existing = await store.getTransactions(tenantId) as Array<{ externalId?: string }>; const alreadySynced = existing.some(t => t.externalId === payment.id); if (!alreadySynced) { - await storage.createTransaction({ + await store.createTransaction({ tenantId, accountId: 'rent-income-account', // Would map correctly amount: payment.amount.toString(), @@ -380,11 +382,11 @@ export class ChittyRentalClient { for (const expense of expenses) { try { // Check if already synced - const existing = await storage.getTransactions(tenantId); + const existing = await store.getTransactions(tenantId) as Array<{ externalId?: string }>; const alreadySynced = existing.some(t => t.externalId === expense.id); if (!alreadySynced) { - await storage.createTransaction({ + await store.createTransaction({ tenantId, accountId: 'property-expense-account', amount: (-expense.amount).toString(), diff --git a/server/lib/chittyschema-validation.ts b/server/lib/chittyschema-validation.ts index 6436b17..24559fe 100644 --- a/server/lib/chittyschema-validation.ts +++ b/server/lib/chittyschema-validation.ts @@ -1,4 +1,4 @@ -// @ts-nocheck - TODO: Add proper types + /** * ChittySchema Integration for ChittyFinance * Validates financial data against centralized ChittyOS schema service @@ -54,7 +54,7 @@ export async function validateWithChittySchema( } ); - const result = await response.json(); + const result = await response.json() as ValidationResult; return result; } catch (error) { console.error('ChittySchema validation error:', error); @@ -84,7 +84,7 @@ export async function getEntityTypes(): Promise { } ); - const result = await response.json(); + const result = await response.json() as { types?: EntityTypeInfo[] }; return result.types || []; } catch (error) { console.error('ChittySchema entity types fetch error:', error); @@ -110,11 +110,11 @@ export async function getSchemaDetails(entityType: string): Promise { * Express middleware for rate limiting */ export function rateLimitMiddleware(limiter: RateLimiter) { - return async (req: Request, res: Response, next: NextFunction) => { + return async (req: Request, res: ExpressResponse, next: NextFunction) => { const key = req.ip || req.socket.remoteAddress || 'unknown'; const result = await limiter.check(key); @@ -192,7 +192,7 @@ export function rateLimitMiddleware(limiter: RateLimiter) { /** * Express error handling middleware */ -export function errorHandler(err: Error, req: Request, res: Response, _next: NextFunction) { +export function errorHandler(err: Error, req: Request, res: ExpressResponse, _next: NextFunction) { console.error('Error:', err); // Handle known error types @@ -233,8 +233,8 @@ export function errorHandler(err: Error, req: Request, res: Response, _next: Nex /** * Wrapper for async route handlers */ -export function asyncHandler(fn: (req: Request, res: Response, next: NextFunction) => Promise) { - return (req: Request, res: Response, next: NextFunction) => { +export function asyncHandler(fn: (req: Request, res: ExpressResponse, next: NextFunction) => Promise) { + return (req: Request, res: ExpressResponse, next: NextFunction) => { Promise.resolve(fn(req, res, next)).catch(next); }; } diff --git a/server/lib/openaiBudget.ts b/server/lib/openaiBudget.ts index 51f23e3..ed5585e 100644 --- a/server/lib/openaiBudget.ts +++ b/server/lib/openaiBudget.ts @@ -1,4 +1,4 @@ -// @ts-nocheck - TODO: Add proper types + // Weekly token budget guard for OpenAI usage // Supports per-user persistent tracking via DB and a global in-memory fallback. import { db } from "../db"; diff --git a/server/lib/reconciliation.ts b/server/lib/reconciliation.ts index a2dffc2..d7b3631 100644 --- a/server/lib/reconciliation.ts +++ b/server/lib/reconciliation.ts @@ -1,4 +1,4 @@ -// @ts-nocheck - TODO: Add proper types + /** * Reconciliation Module for ChittyFinance * Provides bank reconciliation and transaction matching functionality @@ -7,6 +7,8 @@ import { storage } from '../storage'; import { logReconciliation } from './chittychronicle-logging'; +const store = storage as any; + export interface ReconciliationMatch { internalTransaction: { id: string; @@ -207,14 +209,15 @@ export async function reconcileAccount( endDate: Date ): Promise { // Get internal transactions for the period - const allTransactions = await storage.getTransactions(tenantId); + interface StoredTransaction { id: string; date: string | Date; amount: string; description: string; externalId?: string | null; accountId?: string } + const allTransactions: StoredTransaction[] = await store.getTransactions(tenantId); const internalTransactions = allTransactions - .filter(t => + .filter((t: StoredTransaction) => t.accountId === accountId && new Date(t.date) >= startDate && new Date(t.date) <= endDate ) - .map(t => ({ + .map((t: StoredTransaction) => ({ id: t.id, date: new Date(t.date), amount: parseFloat(t.amount), @@ -238,7 +241,7 @@ export async function reconcileAccount( const difference = statementBalance - bookBalance; // Get account info - const account = await storage.getAccount(accountId); + const account = await store.getAccount(accountId, tenantId) as { name?: string } | undefined; return { accountId, diff --git a/server/lib/storage-helpers.ts b/server/lib/storage-helpers.ts index 834b94d..dd6464c 100644 --- a/server/lib/storage-helpers.ts +++ b/server/lib/storage-helpers.ts @@ -1,4 +1,4 @@ -// @ts-nocheck - TODO: Add proper types + /** * Storage Helpers - Smart wrappers for multi-tenant storage access * @@ -10,18 +10,25 @@ import type { Request } from 'express'; import { storage } from '../storage'; import { toStringId } from './id-compat'; +const store = storage as any; + +interface StorageRequest extends Request { + userId?: string | number; + tenantId?: string | number; +} + const MODE = process.env.MODE || 'standalone'; /** * Get the appropriate storage context from a request */ -export function getStorageContext(req: Request): { +export function getStorageContext(req: StorageRequest): { userId: string; tenantId: string; mode: 'standalone' | 'system'; } { const mode = MODE === 'system' ? 'system' : 'standalone'; - const userId = toStringId(req.userId || (req as any).userId || 1); + const userId = toStringId(req.userId || 1); const tenantId = toStringId(req.tenantId || userId); // In standalone, userId === tenantId return { @@ -34,16 +41,16 @@ export function getStorageContext(req: Request): { /** * Get integrations for current user/tenant */ -export async function getIntegrations(req: Request) { +export async function getIntegrations(req: StorageRequest) { const ctx = getStorageContext(req); - return storage.getIntegrations(ctx.tenantId); + return store.getIntegrations(ctx.tenantId); } /** * Get single integration */ export async function getIntegration(req: Request, id: number | string) { - return storage.getIntegration(toStringId(id)); + return store.getIntegration(toStringId(id)); } /** @@ -52,7 +59,7 @@ export async function getIntegration(req: Request, id: number | string) { export async function createIntegration(req: Request, data: any) { const ctx = getStorageContext(req); - return storage.createIntegration({ + return store.createIntegration({ ...data, tenantId: ctx.tenantId, userId: MODE === 'standalone' ? parseInt(ctx.userId, 10) : undefined, @@ -63,7 +70,7 @@ export async function createIntegration(req: Request, data: any) { * Update integration */ export async function updateIntegration(req: Request, id: number | string, data: any) { - return storage.updateIntegration(toStringId(id), data); + return store.updateIntegration(toStringId(id), data); } /** @@ -71,14 +78,14 @@ export async function updateIntegration(req: Request, id: number | string, data: */ export async function getTasks(req: Request, limit?: number) { const ctx = getStorageContext(req); - return storage.getTasks(ctx.tenantId, limit); + return store.getTasks(ctx.tenantId, limit); } /** * Get single task */ export async function getTask(req: Request, id: number | string) { - return storage.getTask(toStringId(id)); + return store.getTask(toStringId(id)); } /** @@ -87,7 +94,7 @@ export async function getTask(req: Request, id: number | string) { export async function createTask(req: Request, data: any) { const ctx = getStorageContext(req); - return storage.createTask({ + return store.createTask({ ...data, tenantId: ctx.tenantId, userId: MODE === 'standalone' ? parseInt(ctx.userId, 10) : ctx.userId, @@ -98,7 +105,7 @@ export async function createTask(req: Request, data: any) { * Update task */ export async function updateTask(req: Request, id: number | string, data: any) { - return storage.updateTask(toStringId(id), data); + return store.updateTask(toStringId(id), data); } /** @@ -106,7 +113,7 @@ export async function updateTask(req: Request, id: number | string, data: any) { */ export async function getAiMessages(req: Request, limit?: number) { const ctx = getStorageContext(req); - return storage.getAiMessages(ctx.tenantId, ctx.userId, limit); + return store.getAiMessages(ctx.tenantId, ctx.userId, limit); } /** @@ -115,7 +122,7 @@ export async function getAiMessages(req: Request, limit?: number) { export async function createAiMessage(req: Request, data: any) { const ctx = getStorageContext(req); - return storage.createAiMessage({ + return store.createAiMessage({ ...data, tenantId: MODE === 'standalone' ? parseInt(ctx.tenantId, 10) : ctx.tenantId, userId: MODE === 'standalone' ? parseInt(ctx.userId, 10) : ctx.userId, @@ -127,7 +134,7 @@ export async function createAiMessage(req: Request, data: any) { */ export async function getTransactions(req: Request, limit?: number) { const ctx = getStorageContext(req); - return storage.getTransactions(ctx.tenantId, limit); + return store.getTransactions(ctx.tenantId, limit); } /** @@ -136,7 +143,7 @@ export async function getTransactions(req: Request, limit?: number) { export async function createTransaction(req: Request, data: any) { const ctx = getStorageContext(req); - return storage.createTransaction({ + return store.createTransaction({ ...data, tenantId: MODE === 'standalone' ? parseInt(ctx.tenantId, 10) : ctx.tenantId, userId: MODE === 'standalone' ? parseInt(ctx.userId, 10) : undefined, @@ -146,12 +153,12 @@ export async function createTransaction(req: Request, data: any) { /** * Get financial summary for current user/tenant */ -export async function getFinancialSummary(req: Request) { +export async function getFinancialSummary(req: StorageRequest) { const ctx = getStorageContext(req); - if (!storage.getFinancialSummary) { + if (!store.getFinancialSummary) { return undefined; // System mode doesn't have this method } - return storage.getFinancialSummary(ctx.userId); + return store.getFinancialSummary(ctx.userId); } diff --git a/server/lib/stripe-connect.ts b/server/lib/stripe-connect.ts index e30d55b..0f3e31c 100644 --- a/server/lib/stripe-connect.ts +++ b/server/lib/stripe-connect.ts @@ -1,4 +1,4 @@ -// @ts-nocheck - TODO: Add proper types + /** * Stripe Connect Integration for ChittyFinance * Access financial data from connected Stripe accounts @@ -9,7 +9,7 @@ import { storage } from '../storage'; import { logToChronicle } from './chittychronicle-logging'; import { validateTransaction } from './chittyschema-validation'; -const STRIPE_API_VERSION: Stripe.LatestApiVersion = '2024-06-20' as any; +const store = storage as any; export interface StripeConnectedAccount { id: string; @@ -19,9 +19,7 @@ export interface StripeConnectedAccount { name?: string; url?: string; }; - capabilities?: { - [key: string]: string; - }; + capabilities?: Record; chargesEnabled: boolean; payoutsEnabled: boolean; defaultCurrency: string; @@ -42,7 +40,7 @@ export interface StripeTransaction { description: string; status: string; customer?: string; - metadata?: Record; + metadata?: Record | null; } /** @@ -56,7 +54,7 @@ export class StripeConnectClient { if (!key) { throw new Error('STRIPE_SECRET_KEY is required'); } - this.stripe = new Stripe(key, { apiVersion: STRIPE_API_VERSION }); + this.stripe = new Stripe(key); } /** @@ -73,7 +71,7 @@ export class StripeConnectClient { name: account.business_profile.name || undefined, url: account.business_profile.url || undefined, } : undefined, - capabilities: account.capabilities || {}, + capabilities: (account.capabilities || {}) as Record, chargesEnabled: account.charges_enabled || false, payoutsEnabled: account.payouts_enabled || false, defaultCurrency: account.default_currency || 'usd', @@ -94,7 +92,7 @@ export class StripeConnectClient { name: account.business_profile.name || undefined, url: account.business_profile.url || undefined, } : undefined, - capabilities: account.capabilities || {}, + capabilities: (account.capabilities || {}) as Record, chargesEnabled: account.charges_enabled || false, payoutsEnabled: account.payouts_enabled || false, defaultCurrency: account.default_currency || 'usd', @@ -317,7 +315,7 @@ export class StripeConnectClient { for (const transaction of transactions) { try { // Check if already synced - const existing = await storage.getTransactions(tenantId); + const existing: Array<{ externalId?: string }> = await store.getTransactions(tenantId); const alreadySynced = existing.some( t => t.externalId === `stripe-${transaction.type}-${transaction.id}` ); @@ -381,7 +379,7 @@ export class StripeConnectClient { console.warn(`ChittySchema validation unavailable for ${transaction.type} ${transaction.id}:`, error); } - await storage.createTransaction(transactionData); + await store.createTransaction(transactionData); synced++; } diff --git a/server/lib/wave-api.ts b/server/lib/wave-api.ts index 4a1cd66..c9789a4 100755 --- a/server/lib/wave-api.ts +++ b/server/lib/wave-api.ts @@ -213,7 +213,7 @@ export class WaveAPIClient { /** * Execute GraphQL query */ - private async graphql(query: string, variables?: Record): Promise { + protected async graphql(query: string, variables?: Record): Promise { if (!this.accessToken) { throw new Error('Wave API: Access token not set. Call setAccessToken() first.'); } diff --git a/server/lib/wave-bookkeeping.ts b/server/lib/wave-bookkeeping.ts index 037733e..bc01260 100644 --- a/server/lib/wave-bookkeeping.ts +++ b/server/lib/wave-bookkeeping.ts @@ -1,4 +1,4 @@ -// @ts-nocheck - TODO: Add proper types + /** * Enhanced Wave Bookkeeping Integration * Comprehensive bookkeeping features: invoices, bills, payments, tax tracking @@ -9,6 +9,9 @@ import { storage } from '../storage'; import { logToChronicle } from './chittychronicle-logging'; import { validateTransaction } from './chittyschema-validation'; +// Cast storage for cross-cutting calls that work across both modes +const store = storage as any; + export interface WaveInvoice { id: string; invoiceNumber: string; @@ -90,7 +93,7 @@ export interface WaveVendor { }; } -export interface WaveAccount { +export interface WaveBookkeepingAccount { id: string; name: string; type: string; @@ -150,7 +153,7 @@ export class WaveBookkeepingClient extends WaveAPIClient { /** * Get all invoices with detailed information */ - async getInvoices(businessId: string, options?: { + async getDetailedInvoices(businessId: string, options?: { status?: string; customerId?: string; startDate?: string; @@ -312,7 +315,7 @@ export class WaveBookkeepingClient extends WaveAPIClient { const data = await this.graphql<{ invoiceCreate: { invoice: any } }>(mutation, { input }); // Fetch full invoice details - const invoices = await this.getInvoices(businessId, { + const invoices = await this.getDetailedInvoices(businessId, { pageSize: 1, }); @@ -468,7 +471,7 @@ export class WaveBookkeepingClient extends WaveAPIClient { /** * Get chart of accounts */ - async getAccounts(businessId: string): Promise { + async getBookkeepingAccounts(businessId: string): Promise { const query = ` query GetAccounts($businessId: ID!) { business(id: $businessId) { @@ -518,7 +521,7 @@ export class WaveBookkeepingClient extends WaveAPIClient { endDate: string ): Promise { // Get invoices (revenue) and expenses - const invoices = await this.getInvoices(businessId, { startDate, endDate, status: 'PAID' }); + const invoices = await this.getDetailedInvoices(businessId, { startDate, endDate, status: 'PAID' }); const expenses = await this.getExpenses(businessId, 1, 200); // Filter expenses by date @@ -566,13 +569,13 @@ export class WaveBookkeepingClient extends WaveAPIClient { try { // Sync invoices as income transactions - const invoices = await this.getInvoices(businessId, { status: 'PAID' }); + const invoices = await this.getDetailedInvoices(businessId, { status: 'PAID' }); for (const invoice of invoices) { try { // Check if already synced - const existing = await storage.getTransactions(tenantId); - const alreadySynced = existing.some(t => t.externalId === invoice.id); + const existing = await store.getTransactions(tenantId) as Array<{ externalId?: string }>; + const alreadySynced = existing.some((t: { externalId?: string }) => t.externalId === invoice.id); if (!alreadySynced) { const transactionData = { @@ -604,7 +607,7 @@ export class WaveBookkeepingClient extends WaveAPIClient { console.warn(`ChittySchema validation unavailable for invoice ${invoice.id}:`, error); } - await storage.createTransaction(transactionData); + await store.createTransaction(transactionData); synced.invoices++; } } catch (error) { @@ -617,8 +620,8 @@ export class WaveBookkeepingClient extends WaveAPIClient { for (const expense of expenses) { try { - const existing = await storage.getTransactions(tenantId); - const alreadySynced = existing.some(t => t.externalId === expense.id); + const existing = await store.getTransactions(tenantId) as Array<{ externalId?: string }>; + const alreadySynced = existing.some((t: { externalId?: string }) => t.externalId === expense.id); if (!alreadySynced) { const transactionData = { @@ -634,7 +637,7 @@ export class WaveBookkeepingClient extends WaveAPIClient { reconciled: true, metadata: { source: 'wave', - vendorId: expense.vendor?.id, + vendorName: expense.vendor?.name, }, }; @@ -649,7 +652,7 @@ export class WaveBookkeepingClient extends WaveAPIClient { console.warn(`ChittySchema validation unavailable for expense ${expense.id}:`, error); } - await storage.createTransaction(transactionData); + await store.createTransaction(transactionData); synced.expenses++; } } catch (error) {