name = "zync-api"
main = "src/index.ts"
compatibility_date = "2026-05-30"
# nodejs_compat: required by otplib's @otplib/plugin-crypto (TOTP), which calls
# require('crypto'). Under this flag Workers resolves node:crypto natively, so the
# admin-2fa routes bundle. WebCrypto (crypto.subtle) is used everywhere else.
compatibility_flags = ["nodejs_compat"]

# Workers Paid plan required: Durable Objects + Vectorize
# Placement routes each request to the PoP nearest to Neon (zero added cost)
[placement]
mode = "smart"

# ── Cloudflare Bindings ───────────────────────────────────────────────────────

# Hyperdrive → Neon Postgres  (binding name: DB)
[[hyperdrive]]
binding = "DB"
id      = "1af96f9b5ba94e1ba112dc6979c39c7b"

# R2 bucket — file / attachment storage  (binding name: STORAGE)
[[r2_buckets]]
binding     = "STORAGE"
bucket_name = "zync-storage"

# expenses-module: R2 bucket for receipt images  (binding name: RECEIPTS_BUCKET)
[[r2_buckets]]
binding     = "RECEIPTS_BUCKET"
bucket_name = "zync-receipts"

# invoices-core (wave-6): R2 bucket for immutable HTML invoice snapshots
[[r2_buckets]]
binding     = "INVOICE_SNAPSHOTS_BUCKET"
bucket_name = "zync-invoice-snapshots"

# KV namespace — session cache, token revocation, counters  (binding name: KV)
[[kv_namespaces]]
binding = "KV"
id      = "48f10b30621144d59078772f7f3c9e5a"

# system-status-page: public status cache KV (30s TTL)  (binding name: STATUS_KV)
[[kv_namespaces]]
binding = "STATUS_KV"
id      = "4be9179f9e154f79b1002fdd8b96ae17"

# wave-12: oauth-authorization-code — token endpoint rate limiting  (binding name: RATELIMIT_KV)
[[kv_namespaces]]
binding = "RATELIMIT_KV"
id      = "8bf026494e444641a0229d7ed14181d7"

# Queue producer — async job dispatch  (binding name: QUEUE)
[[queues.producers]]
binding = "QUEUE"
queue   = "zync-jobs"

# Workers AI  (binding name: AI)
[ai]
binding = "AI"

# Durable Object — per-tenant WebSocket hub  (binding name: DO_REALTIME)
[[durable_objects.bindings]]
name       = "DO_REALTIME"
class_name = "TenantRealtimeDO"

# Durable Object — PBKDF2 derive offload  (binding name: PWHASH_DO)
[[durable_objects.bindings]]
name       = "PWHASH_DO"
class_name = "PasswordHashDO"

# Durable Object — login / signup / verify-email WRITE offload  (binding name: AUTHWRITE_DO)
[[durable_objects.bindings]]
name       = "AUTHWRITE_DO"
class_name = "AuthWriteDO"

# Durable Object — self-rescheduling recurring-invoice generator  (binding name: RECURRING_ALARM_DO)
[[durable_objects.bindings]]
name       = "RECURRING_ALARM_DO"
class_name = "RecurringInvoiceAlarmDO"

# DO migrations — required for wrangler to accept the DO binding declaration
[[migrations]]
tag = "v1"
new_sqlite_classes = ["TenantRealtimeDO"]

[[migrations]]
tag = "v2"
new_sqlite_classes = ["PasswordHashDO"]

[[migrations]]
tag = "v3"
new_sqlite_classes = ["AuthWriteDO"]

[[migrations]]
tag = "v4"
new_sqlite_classes = ["RecurringInvoiceAlarmDO"]

[[migrations]]
tag = "v5"
new_sqlite_classes = ["ApiRuntimeDO"]

[[migrations]]
tag = "v6"
deleted_classes = ["ApiRuntimeDO"]

# Vectorize index — per-tenant RAG embeddings  (binding name: VECTORIZE)
[[vectorize]]
binding    = "VECTORIZE"
index_name = "zync-embeddings"

# Analytics Engine  (binding name: ANALYTICS_ENGINE)
[[analytics_engine_datasets]]
binding = "ANALYTICS_ENGINE"
dataset = "zync-analytics"

# wave-12: api-usage-quota-ui — per-key request counter (write binding)
[[analytics_engine_datasets]]
binding = "API_USAGE"
dataset = "api_usage"

# ── Native Rate Limiters (CF Paid) ────────────────────────────────────────────
[[ratelimits]]
name         = "RATE_LIMITER_AUTH"
namespace_id = "1001"
[ratelimits.simple]
limit  = 5
period = 60

[[ratelimits]]
name         = "RATE_LIMITER_WEBHOOK"
namespace_id = "1002"
[ratelimits.simple]
limit  = 100
period = 60

[[ratelimits]]
name         = "RATE_LIMITER_EXPENSE_UPLOAD"
namespace_id = "1003"
[ratelimits.simple]
limit  = 10
period = 60

[[ratelimits]]
name         = "RATE_LIMITER_LEAD_FORM"
namespace_id = "1004"
[ratelimits.simple]
limit  = 20
period = 60

[[ratelimits]]
name         = "RATE_LIMITER_SEARCH"
namespace_id = "1005"
[ratelimits.simple]
limit  = 60
period = 60

[[ratelimits]]
name         = "RATE_LIMITER_SIGN"
namespace_id = "1101"
[ratelimits.simple]
limit  = 20
period = 60

[[ratelimits]]
name         = "RATE_LIMITER_PROPOSAL"
namespace_id = "1102"
[ratelimits.simple]
limit  = 10
period = 60

[[ratelimits]]
name         = "RATE_LIMITER_CATALOG"
namespace_id = "1103"
[ratelimits.simple]
limit  = 30
period = 60

# wave-11 leaf-C: portal files R2 bucket
[[r2_buckets]]
binding     = "PORTAL_FILES_BUCKET"
bucket_name = "zync-portal-files"

# ── Queue Producers (message types on shared zync-jobs) ───────────────────────
# The canonical Env interface exposes exactly ONE queue binding: QUEUE → zync-jobs.
# Domain specs enqueue async work via message type, not separate Env bindings:
#   env.QUEUE.send({ type: 'webhook.deliver', ... })
#   env.QUEUE.send({ type: 'payment.charge', ... })
# CF queue names cannot contain dots (^ [a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$).
# The dotted identifiers below are MESSAGE TYPES routed by the zync-jobs consumer
# (see apps/zync-api/src/index.ts queue handler). They are NOT Cloudflare queue names.
#
# Reserved message types (handled in zync-jobs consumer by batch.messages[].type):
#   payment.charge       — billing-engine spec
#   campaign.send_batch  — campaigns-engine spec
#   webhook.deliver      — white-label-api / webhooks-engine spec
#   export.generate      — data-export spec (uniform-format, report.schedule, etc.)

[[queues.consumers]]
queue          = "zync-jobs"
max_batch_size = 10
max_retries    = 5

# comms.inbound — inbound webhook messages (Telegram/Slack/WhatsApp)
# Owned by system-communications-notifications. Producer uses the QUEUE binding.
[[queues.consumers]]
queue            = "comms-inbound"
max_batch_size   = 10
max_retries      = 5
dead_letter_queue = "comms-inbound-dlq"

# tenant-audit-log: AUDIT_QUEUE producer binding
[[queues.producers]]
binding = "AUDIT_QUEUE"
queue   = "audit-log-queue"

# tenant-audit-log: audit event consumer
[[queues.consumers]]
queue          = "audit-log-queue"
max_retries    = 3
max_batch_size = 100

# ai-assistant: AI index update consumer
[[queues.consumers]]
queue            = "ai-index-update"
max_batch_size   = 25
max_retries      = 5
dead_letter_queue = "ai-index-update-dlq"

# ai-assistant: Telegram message consumer
[[queues.consumers]]
queue            = "ai-telegram-message"
max_batch_size   = 10
max_retries      = 3
dead_letter_queue = "ai-telegram-message-dlq"

# expenses-module: expense processing consumer
[[queues.consumers]]
queue          = "expense-process"
max_batch_size = 10
max_retries    = 3

[[queues.producers]]
queue   = "expense-process"
binding = "EXPENSE_QUEUE"

# crm-support-center (wave-6): reply dispatch queue
[[queues.producers]]
binding = "SUPPORT_REPLY_DISPATCH_QUEUE"
queue   = "support-reply-dispatch"

# real-time-infrastructure: WS event fan-out queue.
# Producer binding REALTIME_QUEUE; consumer (in this Worker) routes batches to
# the per-tenant TenantRealtimeDO via broadcastToTenant. Events are ephemeral.
[[queues.producers]]
binding = "REALTIME_QUEUE"
queue   = "zync-realtime"

[[queues.consumers]]
queue          = "zync-realtime"
max_batch_size = 25
max_retries    = 3

# Secrets declared via wrangler secret put (system-communications-notifications):
#   wrangler secret put RESEND_API_KEY          — system email via Resend REST API
#   wrangler secret put INTEGRATION_ENCRYPTION_KEY — AES-256 key for adapter credentials
#   wrangler secret put VAPID_PRIVATE_KEY       — PKCS#8 P-256 key (base64url)
#   wrangler secret put VAPID_PUBLIC_KEY        — Uncompressed P-256 pubkey (base64url)

# ── AI Provider Secrets (system-ai) ──────────────────────────────────────────
# Declare via: wrangler secret put OPENAI_API_KEY
#              wrangler secret put GOOGLE_AI_API_KEY
# ANTHROPIC_API_KEY already declared; these two are new.
# All three are read by @zync/ai adapters server-side (Worker → provider API).
# No CSP change needed: provider calls are Worker→API, NOT browser→provider.

# ── Cron Triggers ─────────────────────────────────────────────────────────────
# PENDING: CF account at 5-cron free-plan limit (multideal-do-preview=4, vibeflare=1).
# Re-enable after plan upgrade or clearing other workers' crons.
# [triggers]
# crons = [
#   "*/5 * * * *",   # frequent: campaigns, session-sweep, scheduled-reports
#   "*/15 * * * *",  # ticket-sla
#   "0 * * * *",     # hourly: invoice-adapter-reconcile, tasks-sync, time-cleanup
#   "30 1 * * *",    # daily 01:30 UTC: retention, cleanup, audit
#   "0 5 * * *",     # daily 05:00 UTC: billing, renewals, contracts, calendar-sync, ticket-close
#   "0 9 * * *",     # daily 09:00 UTC: invoicing, leads, score-refresh, recurring generators
# ]

# ── zync-subscription: payment adapter selector ───────────────────────────────
[vars]
ZYNC_PAYMENT_ADAPTER = ""
# invoices-core (wave-6): public R2 custom domain for invoice snapshots
INVOICE_SNAPSHOTS_PUBLIC_URL = ""
# admin-reports-analytics: Cloudflare Analytics Engine SQL API
CF_ACCOUNT_ID = "4c417b8bd59ae45e1fa25ca2d3c59806"
CF_ANALYTICS_DATASET = "zync_funnel"
ADMIN_REPORTS_USD_ILS_RATE = "3.7"
# tenant-logo public route base (must route /api/* to zync-api)
APP_BASE_URL = "https://app.dev.zync.is"

# Workers dev subdomain (always-on for smoke testing; add zone routes once zync.is zone is in CF)
workers_dev = true

# ── Environments ──────────────────────────────────────────────────────────────

[env.preview]
name = "zync-api-preview"
compatibility_date = "2026-05-30"
compatibility_flags = ["nodejs_compat"]
workers_dev = true
[env.preview.vars]
ENVIRONMENT = "preview"
FIREBASE_PROJECT_ID = "zync-dev-dd678"
APP_BASE_URL = "https://zync-app-preview.dry-salad-ffa1.workers.dev"

# Preview Hyperdrive and KV IDs are intentionally not stored here. PR CI builds
# a fail-closed ephemeral config from isolated GitHub environment secrets.
