Skip to content

Date-based token endpoint, soft-delete contacts for RESTORE, redact presigned URLs - #3

Merged
zprobs merged 5 commits into
mainfrom
journal-auth-softdelete-redaction
Oct 21, 2026
Merged

zprobs merged 5 commits into
mainfrom
journal-auth-softdelete-redaction

Conversation

@zprobs

@zprobs zprobs commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Date-based token endpoint. The client-credentials token now comes from /oauth/2026-09/token instead of /oauth/v1/token, so the example uses date-based versions throughout. The request body and the access_token/expires_in response fields are unchanged. The install and refresh flow (authorization code) is untouched.
  • Soft deletes, so RESTORE restores. A RESTORE event carries only the properties changed by the restore itself, usually none. With hard deletes, a restored contact came back empty. DELETE now sets deletedAt and keeps the row, and RESTORE clears it, so the contact's stored data returns.
    • A DELETE for a contact we've never seen writes a tombstone, so an older out-of-order CREATE processed later by another worker can't resurrect it.
    • MERGE still hard-deletes merged-away contacts, because merges are permanent.
    • The list API excludes soft-deleted rows.
    • A code comment describes the alternative for when there's no row to restore: request a single-object snapshot (POST .../snapshots/<ver>/crm).
  • Presigned URLs are credentials. A failed journal-file download now reports its status and the file's origin and path, with the query string replaced by ?<redacted>. Logging the full URL would leak the signature. The README notes this.

BRAVE Checklist

Backwards compatibility: needs a schema update. Existing checkouts must re-run npm run db-init (prisma db push) to add the nullable Contact.deletedAt column, and the README says so. Existing rows are unaffected (deletedAt stays NULL).

Roll(out/back) plan: example app with no deployment. Revert the PR to undo. The extra column is harmless if left in place.

Automated testing:

  • __tests__/processor.test.ts: 8 integration tests against real Postgres covering the DELETE/RESTORE round trip, an out-of-order UPDATE after a DELETE, a tombstone blocking an older CREATE, a newer UPDATE or SNAPSHOT un-deleting, MERGE still hard-deleting, and GET /api/contacts excluding soft-deleted rows. They run in CI against the workflow's webhooks_journal_test DB and skip when DATABASE_URL doesn't point at a _test database.
  • __tests__/download.test.ts and __tests__/utils.test.ts: redaction tests.
  • Full suite: 34/34 with a test DB, 26 passing and 8 skipped without one.

Verification plan: ran the app locally against prod.

  • At startup it fetched a client-credentials token from /oauth/2026-09/token, and journal polling with that token worked.
  • Deleting a contact in a test portal produced Soft-deleted contact …: it disappeared from /api/contacts, but its email and name stayed in the row.
  • Restoring it from the HubSpot recycle bin produced Restored contact …: it reappeared with its email and name, which the RESTORE event did not carry.

Expect dependencies to fail: if the token endpoint fails, the existing error path surfaces it, unchanged. If a journal-file download fails, the job fails with a message naming the file without its credential, and BullMQ's existing retry and backoff applies.

Zach Probst added 5 commits October 21, 2026 16:08
The v1 token path is being superseded by /oauth/2026-09/token; the request and
response shapes are unchanged, so only the URL moves (into a shared constant).
…credential

Journal file URLs are presigned S3 URLs whose query string is the credential. A
failed download previously gave no hint which file failed, and logging the URL
to fix that would leak it. downloadJournalFile now rethrows with status and
origin+path only, so the worker's failed handler logs a safe, useful message.
A RESTORE journal event only carries the properties that action changed,
so recreating a hard-deleted contact from it left the row mostly empty.
DELETE now sets deletedAt (and writes a tombstone for unseen contacts so an
older out-of-order CREATE/UPDATE can't resurrect it), upserts clear
deletedAt, and the contacts API hides deleted rows. MERGE still hard-deletes
merged IDs since they never come back.
@zprobs
zprobs marked this pull request as ready for review October 21, 2026 20:52
@zprobs
zprobs merged commit b5060d0 into main Oct 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant