Releases: mirza-rizvi/ResolveHQ
Releases · mirza-rizvi/ResolveHQ
Release list
v0.3.3
Changed
- The CPU guidance now carries a real measurement instead of an inference. A Free-plan operator measured a signup at 29 ms of CPU against the plan's 10 ms budget, and saw several other paths over 10 ms on a workspace with almost no data; it works because Cloudflare tolerates infrequent overage, not because it fits. The README, the deployment guide and the Free-plan audit say that, and say to plan on Workers Paid before real volume arrives rather than after. Thanks to the reporter in #9 for the number, which is the only CPU measurement this project has ever had from real infrastructure.
Security
- The deployment guide now states plainly what a webhook destination check does and does not do: it is lexical and resolves no DNS, so a hostname pointing at private space is not refused by that code. Cloudflare's egress and
global_fetch_strictly_publicare what stand behind it. - The deployment guide and the export page now say that erasing a customer does not reach into exports already taken. Delete exports made before an erasure request, or shorten the retention window.
global_fetch_strictly_publicis enabled. A request this Worker makes to its own zone now loops back through Cloudflare's front door instead of being routed straight to the origin, where it would bypass Cloudflare's security settings. It narrows what a tenant-supplied webhook URL can reach; it is not a private-address block, so the checks insrc/server/webhooks/destination.tsstill carry that job.
Added
- The sign-in page says so when the deployment cannot serve requests yet.
/setupand the readiness report both need an admin session, so the one person a broken deployment strands, the operator who cannot create the first account, previously saw nothing but a failed form. npm run smoke -- <url>smoke-tests a deployed Worker: that it responds, that its database has a schema, and that the app shell is served. With--signupit also creates one throwaway workspace and signs in with it, which is the only check that exercises password hashing on real infrastructure. Every other gate in the project runs against localworkerd, which does not enforce some limits the production runtime does; that gap shipped both the 310,000-iteration PBKDF2 failure and an unmigrated database.- CI now runs the Playwright suite as its own job, on a browser it installs itself, and uploads the report when it fails. The browser tests previously ran only on a developer's machine, which is how a broken screenshot spec went unnoticed for days.
Fixed
- Sign-in no longer takes noticeably longer for an email that has an account than for one that does not. The unknown case returned before the password derivation ran, which let anyone measure which addresses are registered; both cases now derive.
- The browser suite runs in CI. Its first two runs there failed at sign-in, because the dev server reads
.dev.vars, which is gitignored and therefore absent on a runner; without itSESSION_PEPPERis unset and every auth route answers503 Authentication is not configured. CI now creates that file the same wayCONTRIBUTING.mdtells a developer to. Failures also uploadtest-results/, without which neither failure could have been diagnosed, and CI gets a slightly larger test timeout than a developer's machine. - A workspace export sizes each page by what the previous one weighed instead of always reading 200 rows. A table of very wide rows — a message allows 100 KB of text and 200 KB of HTML — could exceed the Worker's memory, and because the cursor stays at the page that failed, every later attempt failed in the same place and the export never recovered.
- A webhook endpoint no longer reads its target's response back to you. A failed delivery stored the first 200 bytes of the response body, and the endpoint list and test routes returned it. Combined with destination checks that resolve no DNS, that made a tenant-supplied URL a readable probe rather than a blind one. Only the status is kept now.
- An API key restricted to particular inboxes is now restricted in search too. The ticket routes enforced it;
/api/v1/searchcarried no inbox predicate at all, so a key scoped to one inbox could read subjects from every inbox under the sametickets:readscope. - The MCP
get_customertool honours the key's inbox restriction. Only its recent-tickets sub-query was filtered, so a restricted key could confirm any customer in the workspace existed and read their name, company and every known email address. - An MCP JSON-RPC batch is capped at 20 calls. The rate limit is checked once per HTTP request, so an unbounded batch turned one token into arbitrarily many searches against the workspace's own database budget.
- A failed or abandoned workspace export no longer leaves its partial data in storage forever. The expiry sweep only ever looked at completed exports; it now also reclaims failed ones after a day and exports that have not advanced for six hours.
- Deleting an export keeps its record, marked expired, because the record is what the one-per-day cap counts. Deleting it turned start-then-delete into an unlimited loop. Nothing is downloadable once the objects are gone.
- One workspace can no longer starve every other one's export. The scheduled run picked the oldest running export, so a large or fast-growing workspace held the only slot indefinitely; it now picks the least recently advanced.
- The settings export dropped the wrong key. It compared
readiness_cacheagainst a key that is actuallyreadiness.cache, so the cached readiness report was exported while a comment claimed it was excluded. It now uses the exported constant. /api/readyno longer returns the names of missing tables to unauthenticated callers. The endpoint stays unauthenticated on purpose, so an operator can check a deployment before signing in, but it now reports only how many tables are missing.
v0.3.2
Fixed
- Sign-up and sign-in failed on every deployed Worker (#9). Password hashing asked for 310,000 PBKDF2 iterations; the Workers runtime refuses anything above 100,000 and answers
Pbkdf2 failed: iteration counts above 100000 are not supported. It is a hard ceiling in the runtime, not a CPU budget, so no plan avoided it. Local workerd does not enforce the cap, which is why every test and every local run passed while no deployment could ever create its first account. Derivation now runs at 100,000, the platform maximum. - The demo seed carried password hashes at 310,000 iterations, so the documented demo login would have failed on a deployment for the same reason. Both seeded accounts are regenerated at 100,000; the password is unchanged.
- A stored hash above the runtime ceiling can never be verified on Workers.
verifyPasswordnow refuses it before derivation instead of throwing, so such an account reads as a wrong password and needs a reset rather than returning a 500. - The 0.3.1 notes said the Deploy to Cloudflare button does not create the database schema. That was wrong: it was inferred from a local reproduction rather than from the reported deployment, where the tables were already present. The guide now treats a missing schema as a condition to check with
/api/readyrather than as something the button always causes.
Changed
- The README, the deployment guide and the Free-plan audit now name sign-in as the heaviest CPU path and tell you to measure it, since Workers Free allows 10 ms per request. The audit previously said to upgrade the plan rather than lower iterations, advice written for a CPU constraint that turned out not to be the binding one; the binding one was the runtime's iteration ceiling, which no plan changes. Whether PBKDF2 at 100,000 fits the Free budget is left as a thing to measure rather than asserted either way.
v0.3.1
Fixed
- Signing up on a deployment made with the Deploy to Cloudflare button failed with "Something went wrong" (#9). The button provisions an empty database and never creates the schema — the deployment guide claimed it ran
npm run deploy, which it does not — so the first query died on a table that was never made. The README and the guide now say to apply the migrations once, and how to make every later deploy do it automatically. - A request that fails because the database has no schema now answers
503 database_not_migratedand names the command that fixes it, instead of a blank500. The message reaches the sign-up form, so the person in front of it can see what is wrong. /api/readyno longer reports a healthy database when the schema is missing. It used to runSELECT 1, which succeeds against a database with no tables at all; it now compares the tables the running code expects against the ones that exist and reportsunmigratedwith the count. Settings → Setup & health gained a matching Database schema check, which catches the other half of the problem: new Worker code deployed over an old database.
v0.3.0
Added
- Workspace export. Settings → Workspace export writes every table in your workspace to newline-delimited JSON in the same Cloudflare R2 bucket your attachments already use, one file per table, downloadable from the page that made it. A large workspace is exported a slice at a time across scheduled runs, so nothing times out, and the page says which table it is on rather than showing a bare spinner. One export at a time per workspace and one per day; keep them for as long as you like (30 days by default) and optionally repeat weekly. Passwords, API key hashes, webhook signing secrets and bot tokens are never exported, and neither are sessions or password-reset rows. Attachment files are not included; their records are. It is a point-in-time copy, so rows written after an export starts may be missing. There is deliberately no restore button — a half-applied restore is worse than none — and the deployment guide documents how to load an export back with
wranglerinstead. - Outbound webhooks, with a Slack and Telegram bridge. Add endpoints under Settings → Webhooks and choose which of six events to send: a ticket opened, assigned, changed status, or missed its response target; a customer replied; a customer rated their support. Deliveries to your own service are signed, and the signing secret is shown once; Slack and Telegram use their own secret URL or bot token, and the Telegram bot is configured per workspace rather than shared. Each endpoint shows its own health — when it last delivered, how many failures in a row, and the last error — and one that fails ten times running turns itself off and says so, with a button to turn it back on. Failures retry on a backoff for up to six attempts; an endpoint reporting it is gone is dropped immediately. Payloads carry ticket ids, numbers, and statuses — never message text or customer email addresses. Addresses are checked when you save them and again before every send: private, internal, and cloud-metadata addresses are refused, redirects are not followed, and an address pointing back at your own deployment is rejected.
- A Model Context Protocol server at
/api/mcp, so Claude Code, Claude Desktop, or Cursor can look things up in your helpdesk. Five tools: search tickets, read one ticket with its thread, list queue counts, look up a customer, and search the knowledge base. Read-only, deliberately — there is no tool that can reply, assign, or change anything, and write tools are held back until there is a screen for approving what an assistant wants to send to a real customer. Connecting needs an API key carrying the MCP permission; Settings builds the exact command or config file for each client, with your deployment's address already filled in. The workspace always comes from the key, a key restricted to particular inboxes sees nothing outside them, and knowledge-base drafts are never returned. - Scoped API keys, so ResolveHQ can be driven by something other than a browser. Admins create them under Settings → API keys, choosing from seven coarse permissions, an optional expiry date, and an optional restriction to particular inboxes. A key is shown once, at creation, and only its prefix is kept afterwards. Keys work against
/api/v1; the surface the app itself uses stays sign-in only, and a request carrying both a session and a key is refused rather than one being silently preferred. A key can never do more than the person who created it: that member's role is re-read on every request, so demoting them immediately reduces every key they issued, and removing them makes those keys stop working and show as inactive. Unknown, revoked and expired keys are all refused with the same message, so a caller cannot learn which of their guesses was once real. - Customer satisfaction ratings, off by default. Turn them on under Settings → Satisfaction ratings and choose the question. When a reply resolves a ticket, three rating links — Bad, OK, Good — are added to the bottom of the email, in both its text and HTML parts. One click from the customer's mail client records the answer and opens a page where they can add a comment. Nothing leaves your Cloudflare account: there is no third-party survey service, no tracking pixel and no external image. Each link is single-use and signed together with its rating, so a rating cannot be edited upward, and a link that has been used, tampered with, or whose ticket has since been deleted shows the customer a calm sentence rather than an error. Scores appear in Reports and the CSV export, always with the number of answers behind them, and ratings are included in a customer's data export and removed when that customer is erased.
- Snooze. Defer a ticket until a date and time, with an optional reason, from the ticket toolbar or with the
Zkey. Presets cover later today, tomorrow, Monday, and next week, and never land on a weekend. A snoozed ticket leaves the Open, Mine, Unassigned, Overdue and Due-soon queues, gets its own Snoozed queue, and is never marked late while it sleeps. It comes back either at its time or the moment the customer replies — whichever is first — and its response targets resume exactly where they paused. Resolved tickets can be snoozed too, as a follow-up reminder; waking one does not reopen it. - Response targets. Create SLA policies with a first-reply target, a resolution target, or both, either as the workspace default or scoped to one priority, and define business hours — a weekly schedule, a timezone, and holidays — under Settings → Response targets. Targets are counted in working minutes, so a ticket that arrives on Friday evening is due on Monday morning rather than overnight. Two new inbox queues, Overdue and Due soon, and a badge on each ticket row show what needs attention; Reports and the CSV export carry breach counts. Nothing is tracked until you create a policy: with none configured, no ticket is ever marked late. A ticket that has been snoozed or already answered is never marked overdue, and changing a ticket's priority recalculates its targets from when the ticket was opened, so a breach cannot be cleared by toggling priority.
- A setup page at
/setup, and a matching Setup & health card in Settings, that tells you whether this deployment can actually receive and send mail. It checks the session secret, mail provider, public address, Turnstile keys, and support inbox, then looks up the MX, SPF, DKIM, and DMARC records of every domain your inboxes use. Each row shows what was found and how to fix it in plain language. A DMARC record is optional, an unreachable resolver reports as unknown rather than as a missing record, and nothing on the page blocks you from working. Results are cached per workspace for ten minutes; Re-check fetches fresh ones. A banner appears in the app while a required check is failing, and disappears once it passes. - Activity entries now record what kind of actor caused them. An automation rule, an AI suggestion, an API-key request, an inbound customer email, and a scheduled job are each distinguishable from a person acting in the app, and the dashboard activity feed labels the non-human ones. Entries caused by an automation show the rule's name rather than an unknown user.
Changed
npm run screenshotsworks again. It signs in through the shared session the rest of the browser suite uses; the capture script was still filling in the sign-in form itself, which never appears once a session exists, so the run timed out and the images in the README had not been refreshed since 0.2.0. They now show this release.- The demo workspace seeds an API key, a webhook endpoint and the export settings, so those sections show a real row instead of an empty state.
- The workspace settings page is now a folder of per-section files rather than one file, so the sections arriving in this release do not keep growing a single page. The page itself, its address, and what it shows are unchanged.
- Activity metadata is filtered before it is stored. A fixed list of sensitive keys — including subjects, message bodies, email addresses, filenames, passwords, tokens, and secrets — is dropped at the single write path, and the entry records which keys were removed. The activity ledger is not covered by customer erasure, so nothing personal should reach it in the first place.
npm run lintno longer fails when a local checkout contains unrelated project clones underdocs/. ESLint 10 walks nested directories looking for configuration files; that path is now ignored.- Applied the open Dependabot updates: Hono, Zod, Tiptap, React 19.3, Playwright, and TypeScript ESLint bumped to their latest minor/patch;
@testing-library/jest-domto 7.0.1;lucide-reactto 1.x (major, icon names unchanged); ESLint to 10 witheslint-plugin-react-hooks7 (adds React Compiler lint rules;set-state-in-effectandrefskept at warn pending a follow-up cleanup pass); GitHub Actionsactions/checkout/actions/setup-nodeto v7 andcloudflare/wrangler-actionto v4 (Wrangler version pin unchanged).
v0.2.0
Added
- Screenshots of the inbox, ticket thread, dashboard, reports, knowledge base, automations, customers, settings, help center, dark mode, and mobile layout in
docs/images/, shown in the README.npm run screenshotsregenerates them from the demo seed. - Customer identities: one customer can have several email addresses. Add or remove addresses on the customer record; admins can merge two customers, which moves tickets, messages, tags, and addresses onto the surviving record.
- Optional Cloudflare Email Sending provider. When the
EMAIL(send_email) binding is present, outgoing mail goes through Cloudflare instead of Resend, and delivery events arrive on theresolvehq-email-eventsqueue. Workers Paid plan; setup steps indocs/deployment.md. - Optional Workers AI provider. When the
AIbinding is present it is used for summaries, drafts, classification, and translation instead of OpenAI.WORKERS_AI_MODELandAI_GATEWAY_IDare supported. - Translation of customer messages and of your draft reply (
POST /api/assistant/translate), with a target language and optional source language. Available with either AI provider once a workspace enables AI assistance. - Optional Cloudflare Turnstile on sign-up, sign-in, and password reset. Active only when both
TURNSTILE_SITE_KEYandTURNSTILE_SECRET_KEYare set. - Optional signed reply address. With
OUTBOUND_REPLY_TOKEN=enabled, replies carry aReply-Toof the formsupport+t<ticket>.<signature>@…so a customer reply threads even when their mail system strips theReferencesheaders. Requires an Email Routing catch-all rule. - GitHub Actions deploy workflow (
.github/workflows/deploy.yml) usingcloudflare/wrangler-action. Runs only whenCLOUDFLARE_API_TOKENis configured as a repository secret. - A fuller demo workspace in
drizzle/seed.sql: nine tickets across every status, knowledge-base articles, automation rules, notifications, and a saved view.
Changed
- Email threading now works across all inboxes of a workspace and across all addresses of a customer. Header matching (
In-Reply-To,References) and the signed reply address accept mail from any of the customer's known addresses, including aReply-Tothat differs fromFrom. The[#1234]subject fallback still requires the original sender address. A reply that matches nothing opens a new ticket; it never lands on another customer's ticket. - Plus-addressed recipients (
support+anything@example.com) resolve to thesupport@example.cominbox. - Local development runs on one server:
npm run devstarts Vite with@cloudflare/vite-plugin, serving the app and the Worker onhttp://localhost:5173.vite previewruns the production build in the Workers runtime. Theconcurrentlydependency and the per-filealiasmap inwrangler.jsoncare gone; Wrangler is now^4.131.1. npm run test:e2eruns only thechromiumproject so it no longer rewrites the committed screenshots.- The Resend webhook and the Cloudflare event consumer share one delivery-state routine (
applyDeliveryEvent). Behaviour of the Resend webhook is unchanged. - The
aiandsend_emailblocks inwrangler.jsoncship commented out. With theaibinding present,wrangler devneedswrangler loginorCLOUDFLARE_API_TOKENbecause Workers AI has no local simulator.
Fixed
- Inbound mail could be attached to another workspace's inbox when two workspaces used the same support address in different letter case. Inbox resolution is now scoped to the owning workspace, and inbox addresses are unique case-insensitively (migration 0006 retires newer case-only duplicates; see the upgrade notes).
wrangler devfailed to start becausewrangler.jsoncwas missing an alias for the organization settings module.- Incoming email staging in local development failed at the R2 upload; the raw message is now written through a fixed-length stream.
- Sending through a provider without an idempotency key could, after a crash, resend the same message. Outbound jobs record when a send was handed to the provider and stop as "delivery uncertain" instead of retrying; an administrator can resend after acknowledging the duplicate risk.
- Translations of long text no longer cut words in half or insert paragraph breaks; a truncated model response fails the request rather than replacing the draft.
Migrations
0006_inbox_tenant_scope,0007_customer_identities,0008_outbound_send_marker. All additive. Read "Case-only duplicate addresses" indocs/deployment.mdbefore applying to a database with existing data.