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.
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)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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
slack_tables.FieldSecurityLock, so user sessions never get them.SlackTokenCipher: AES-256-GCM, bound to its row; keys come fromSLACK_TOKEN_KEYandSLACK_TOKEN_KEY_PREVIOUS; errors are generic.SlackRedactionscrubs error text.SlackClient: the Slack Web API, with a bearer token per call that is never stored.SlackErrorClassifiersorts responses as rate-limited, transient, permanent or auth.SlackAuthorizer(DESIGN 4.1).conversations.membersmust include the owner, with a bounded cache.SlackService: the outbox, dedupe by idempotency key (scoped to the owner),sendOnce, threads and updates.SlackDispatcher:qqq_messagemetadata, giving exactly-once posting;Retry-Afterwithout spending an attempt;QBIT_SLACK, the core messaging provider, under the nameslack. The rich fields areSTAND-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:
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