feat: phase 1b - tables, token cipher, owner/tenant authorization, outbox, dispatcher, core provider #2

Merged
KofTwentyTwo merged 8 commits from feature/outbox-core into develop 2026-10-14 16:54:05 +00:00
KofTwentyTwo commented 2026-10-14 16:19:25 +00:00 (Migrated from github.com)

Phase 1b of docs/DESIGN.md: the tables, the token cipher, owner and tenant authorization, the outbox, the dispatcher and the core provider.

What's in it

  • Seven slack_ tables.
    • Owner locks on connection and message.
    • The host tenant lock on workspace, inherited by join on the others.
    • Token fields are PASSWORD and hidden, with a DENY FieldSecurityLock, so user sessions never get them.
    • Postgres DDL.
  • SlackTokenCipher: AES-256-GCM, bound to its row; keys come from SLACK_TOKEN_KEY and SLACK_TOKEN_KEY_PREVIOUS; errors are generic. SlackRedaction scrubs error text.
  • SlackClient: the Slack Web API, with a bearer token per call that is never stored. SlackErrorClassifier sorts responses as rate-limited, transient, permanent or auth.
  • SlackAuthorizer (DESIGN 4.1).
    • Lookups run in the owner's own session, so tenant and owner locks apply.
    • conversations.members must include the owner, with a bounded cache.
    • It decrypts only the chosen row's token.
    • Ownerless sends are limited to registered channels whose tenant matches.
  • SlackService: the outbox, dedupe by idempotency key (scoped to the owner), sendOnce, threads and updates.
  • SlackDispatcher:
    • claim and lease through attempt rows;
    • re-authorization as the owner at send time;
    • reconcile of expired leases through qqq_message metadata, giving exactly-once posting;
    • 429 handling: wait for Retry-After without spending an attempt;
    • retries and dead letters;
    • FAILED_AUTH, which notifies the owner, raises an alert, and never falls back to another token;
    • the rate limiter and workspace health.
  • QBIT_SLACK, the core messaging provider, under the name slack. The rich fields are STAND-IN(core=QRun-IO/qqq#1194, removeBy=4.1.0-RC.8).

Tests

107 tests pass, Checkstyle is clean, and JaCoCo shows 94.9% instructions and 51/51 classes. They include:

  • Two tenants, two workspaces (they share one Slack team id). User A can't target WB by id, by pinned workspace or by registered name. A's revoked connection gives FAILED_AUTH with an alert to A, and the fake server never sees WB's token on A's traffic. A ciphertext copied across rows fails to decrypt.
  • Crash reconcile: an expired SENDING row whose id is in channel history becomes SENT, with no second post.
  • Token leakage: a canary token is driven through every error path, and it never appears in logs, columns, reasons, exceptions or alerts.
  • Other paths: 429, transient, permanent, max attempts, duplicates, threads, updates, membership lost after queueing, and the provider through core SendMessageAction.

Implemented by Sonnet subagents while Codex's sandbox was down; I reviewed it before each commit. Develop is merged in.

🤖 Generated with Claude Code

Phase 1b of `docs/DESIGN.md`: the tables, the token cipher, owner and tenant authorization, the outbox, the dispatcher and the core provider. ## What's in it - **Seven `slack_` tables.** - Owner locks on connection and message. - The host tenant lock on workspace, inherited by join on the others. - Token fields are PASSWORD and hidden, with a DENY `FieldSecurityLock`, so user sessions never get them. - Postgres DDL. - **`SlackTokenCipher`:** AES-256-GCM, bound to its row; keys come from `SLACK_TOKEN_KEY` and `SLACK_TOKEN_KEY_PREVIOUS`; errors are generic. **`SlackRedaction`** scrubs error text. - **`SlackClient`:** the Slack Web API, with a bearer token per call that is never stored. **`SlackErrorClassifier`** sorts responses as rate-limited, transient, permanent or auth. - **`SlackAuthorizer`** (DESIGN 4.1). - Lookups run in the owner's own session, so tenant and owner locks apply. - `conversations.members` must include the owner, with a bounded cache. - It decrypts only the chosen row's token. - Ownerless sends are limited to registered channels whose tenant matches. - **`SlackService`:** the outbox, dedupe by idempotency key (scoped to the owner), `sendOnce`, threads and updates. - **`SlackDispatcher`:** - claim and lease through attempt rows; - **re-authorization as the owner at send time**; - reconcile of expired leases through `qqq_message` metadata, giving exactly-once posting; - 429 handling: wait for `Retry-After` without spending an attempt; - retries and dead letters; - FAILED_AUTH, which notifies the owner, raises an alert, and never falls back to another token; - the rate limiter and workspace health. - **`QBIT_SLACK`**, the core messaging provider, under the name `slack`. The rich fields are `STAND-IN(core=QRun-IO/qqq#1194, removeBy=4.1.0-RC.8)`. ## Tests 107 tests pass, Checkstyle is clean, and JaCoCo shows 94.9% instructions and 51/51 classes. They include: - **Two tenants, two workspaces** (they share one Slack team id). User A can't target WB by id, by pinned workspace or by registered name. A's revoked connection gives FAILED_AUTH with an alert to A, and the fake server never sees WB's token on A's traffic. A ciphertext copied across rows fails to decrypt. - **Crash reconcile:** an expired SENDING row whose id is in channel history becomes SENT, with no second post. - **Token leakage:** a canary token is driven through every error path, and it never appears in logs, columns, reasons, exceptions or alerts. - **Other paths:** 429, transient, permanent, max attempts, duplicates, threads, updates, membership lost after queueing, and the provider through core `SendMessageAction`. Implemented by Sonnet subagents while Codex's sandbox was down; I reviewed it before each commit. Develop is merged in. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.