nowo-tech / slack-kit-bundle
Slack for Symfony: a hardened Web API client, signed interactions and Events API ingress with replay protection, MAC-signed button values, an interactions router with verified actors, verified account linking, a Block Kit builder and an optional Notifier transport.
Package info
github.com/nowo-tech/SlackKitBundle
Type:symfony-bundle
pkg:composer/nowo-tech/slack-kit-bundle
Fund package maintenance!
Requires
- php: >=8.2 <8.6
- ext-json: *
- nowo-tech/form-kit-bundle: ^2.17
- nowo-tech/ui-kit-bundle: ^2.27
- psr/cache: ^2.0 || ^3.0
- psr/clock: ^1.0
- psr/log: ^2.0 || ^3.0
- symfony/clock: ^7.4 || ^8.0
- symfony/config: ^7.4 || ^8.0
- symfony/dependency-injection: ^7.4 || ^8.0
- symfony/event-dispatcher: ^7.4 || ^8.0
- symfony/form: ^7.4 || ^8.0
- symfony/framework-bundle: ^7.4 || ^8.0
- symfony/http-client: ^7.4 || ^8.0
- symfony/http-client-contracts: ^3.5
- symfony/http-foundation: ^7.4 || ^8.0
- symfony/http-kernel: ^7.4 || ^8.0
- symfony/lock: ^7.4 || ^8.0
- symfony/rate-limiter: ^7.4 || ^8.0
- symfony/routing: ^7.4 || ^8.0
- symfony/security-csrf: ^7.4 || ^8.0
- symfony/service-contracts: ^3.5
- symfony/translation-contracts: ^3.5
- symfony/validator: ^7.4 || ^8.0
- twig/twig: ^3.8
Requires (Dev)
- doctrine/dbal: ^3.8 || ^4.0
- friendsofphp/php-cs-fixer: ^3.0
- igor-php/igor-php: ^0.10.1
- nowo-tech/instance-settings-bundle: ^1.3
- nowo-tech/phpstan-frankenphp: ^1.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^11.5
- rector/rector: ^2.0
- symfony/browser-kit: ^7.4 || ^8.0
- symfony/css-selector: ^7.4 || ^8.0
- symfony/dom-crawler: ^7.4 || ^8.0
- symfony/messenger: ^7.4 || ^8.0
- symfony/notifier: ^7.4 || ^8.0
- symfony/phpunit-bridge: ^7.4 || ^8.0
- symfony/security-bundle: ^7.4 || ^8.0
- symfony/translation: ^7.4 || ^8.0
- symfony/yaml: ^7.4 || ^8.0
Suggests
- nowo-tech/instance-settings-bundle: Store the bot token and signing secret as encrypted instance settings and get the Slack settings section with Test connection (credentials.store: instance_settings)
- symfony/flex: For automatic recipe installation
- symfony/messenger: Handle Events API callbacks in a worker (ingress.async: true) and retry rate-limited handlers after Retry-After (messenger.retry_on_rate_limit)
- symfony/notifier: Send ChatMessage objects through the nowo-slack:// chat transport (notifier.enabled: true)
- symfony/security-bundle: Required by the account link routes and the settings section actions
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-05 17:38:35 UTC
README
β Found this useful? Install from Packagist Β· Give it a star on GitHub so more developers can find it.
Slack for Symfony, hardened. A Web API client with pinned hosts, timeouts and typed rate-limit errors; a signed
interactions and Events API ingress with a body cap, a time window and replay protection; MAC-signed button
values (nsk1) so buttons posted through a leaked incoming-webhook URL cannot be forged; an interactions router
whose handlers get a verified actor (a confirmed, active account) or none; verified account linking with rate
limits; a Block Kit builder with mrkdwn escaping; an admin settings section with Test connection; and an optional
Symfony Notifier chat transport.
π Compatible with Symfony 7.4+ and 8.x (PHP 8.2+; Symfony 8.x requires PHP 8.4+)
FrankenPHP worker mode friendly. Every service is stateless (final readonly) or implements ResetInterface;
credentials are read from the installation store on each call and never memoised across requests; the only
per-request buffer (deferred Slack answers) is flushed on kernel.terminate and reset by kernel.reset. See
docs/FRANKENPHP-WORKER-AUDIT.md.
Table of contents
- Features
- Installation
- Quick start
- Requirements
- Configuration
- Interim decisions shared with symfony-beacon
- Demo
- Development
- Documentation
- Tests and coverage
- License and author
Features
- β
Web API client β
auth.test,conversations.open,chat.postMessage/chat.update/chat.delete,chat.postEphemeral,users.lookupByEmail(opt-in). Constant base URLhttps://slack.com/api/, token only in theAuthorizationheader, no redirects, 5 s idle / 10 s total by default, 1 MiB response cap. - β
Typed errors and rate limits β
SlackApiException,SlackAuthException,SlackScopeException,SlackRateLimitedException(Retry-Afterclamped). Never sleeps; a 429 is remembered per installation and method so other workers do not hammer Slack; optional Messenger middleware retries afterRetry-After. - β
Signed ingress β
POST /slack/interactionsandPOST /slack/events(paths configurable, per-app variants/{app}): body cap before parsing, HMAC v0 withhash_equals, β€ 300 s window on a PSR-20 clock, atomic signature replay memory onsymfony/lock(allowlist of expiring shared stores such as Redis or DBAL; local and session-scoped stores fail the production build),event_idde-duplication, workspace and app id checks, fixed refusal bodies. Clicks verified with a host-resolved (per-destination) secret never get an actor unless you opt in. - β
nsk1button values βnsk1.<payload>.<mac>: canonical JSON, HKDF-SHA256 key fromkernel.secret, 128-bit MAC bound to theaction_id, expiry, optional workspace, verify-only keys after a secret rotation. Byte-compatible with symfony-beacon 1.122.1 (the contract's three test vectors are in the test suite). - β
Interactions router β
#[AsSlackActionHandler(action: 'acme.issue.resolve')]services; handlers get a verifiedSlackInteractionwith a resolvedSlackActoror none; ephemeral answers and message replacement run after the response. - β Verified account linking β typed Slack id or Find me by e-mail, confirmation DM with a signed button, verification only when the clicker is the claimed member; squatting released after 24 h or by a proven claim; four rate limiters; a UiKit account card with CSRF-protected actions.
- β
Credentials β installation store interface; InstanceSettings-backed store (encrypted bot token and signing
secret, admin section with Test connection) or in-memory store; secrets wrapped in
SlackSecret(never dumped, serialised or logged); transport errors keep only a redacted copy of the HttpClient message (SecretRedactor); incoming webhooks go through a logger-free, untraced HttpClient. A shipped contract test case checks host stores. - β
Block Kit builder β sections, headers, context, actions, buttons, link buttons, fields; Slack's limits
enforced (truncate text, refuse structure overflows);
Mrkdwn::escape()by default, fallback text and Notifier subjects included. - β
Optional Notifier transport β
nowo-slack://default?app=default(no secret in the DSN). - β
Translations β
NowoSlackKitBundledomain in en, es, fr, de, it, pt and nl. - β Fakes only β tests, fixtures and the demo use an in-app fake Slack; no real workspace, token or secret.
Installation
composer require nowo-tech/slack-kit-bundle
Register the bundle (Flex does it), import the routes and open the ingress paths in the firewall:
# config/routes/nowo_slack_kit.yaml nowo_slack_kit: resource: . type: nowo_slack_kit
# config/packages/security.yaml security: firewalls: slack: pattern: ^/slack/(interactions|events) security: false
Full guide, including the Slack app manifest with placeholder URLs and the minimum scopes: docs/INSTALLATION.md.
Quick start
use Nowo\SlackKitBundle\Api\SlackApiClientInterface; use Nowo\SlackKitBundle\Api\SlackMessage; use Nowo\SlackKitBundle\BlockKit\Blocks; use Nowo\SlackKitBundle\Credentials\SlackInstallationKey; use Nowo\SlackKitBundle\Id\SlackUserId; use Nowo\SlackKitBundle\Interaction\ButtonValueCodec; public function __construct(private SlackApiClientInterface $slack, private ButtonValueCodec $codec) {} $installation = SlackInstallationKey::default(); $channel = $this->slack->openDirectMessage($installation, SlackUserId::fromString('U00000001')); $value = $this->codec->sign('acme.task.done', ['task' => '42']); $this->slack->postMessage($installation, $channel, new SlackMessage('Task 42 is due', [ Blocks::section('Task *42* is due'), Blocks::actions([Blocks::button('Mark done', 'acme.task.done', $value, 'primary')]), ]));
use Nowo\SlackKitBundle\Interaction\AsSlackActionHandler; use Nowo\SlackKitBundle\Interaction\SlackActionHandlerInterface; use Nowo\SlackKitBundle\Interaction\SlackInteraction; use Nowo\SlackKitBundle\Interaction\SlackInteractionReply; #[AsSlackActionHandler(action: 'acme.task.done')] final readonly class TaskDoneHandler implements SlackActionHandlerInterface { public function handle(SlackInteraction $interaction): SlackInteractionReply { // $interaction->actor->accountId is a verified, active account (requiresActor: true by default) return SlackInteractionReply::done(); } }
Open the admin settings section Slack (InstanceSettings), paste the bot token and the signing secret, press Test connection (it fills the workspace id) and paste the Interactivity URL shown in the section intro into the Slack app. More in docs/USAGE.md.
Requirements
- PHP 8.2+ (
>=8.2 <8.6; Symfony 8.x requires PHP 8.4+) - Symfony 7.4, 8.0 or 8.1 (FrameworkBundle, HttpClient, Lock, RateLimiter, Routing, Form, Validator, Security CSRF, Clock)
- In production, an expiring shared lock store for the replay memory, ideally a dedicated
framework.lock.resources.slack_kitresource (Redis, or a DBAL / PDO DSN without+advisory); a randomkernel.secretof at least 32 bytes nowo-tech/ui-kit-bundle^2.27,nowo-tech/form-kit-bundle^2.17- Suggested:
nowo-tech/instance-settings-bundle(bundled credential store and settings section),symfony/security-bundle(account link routes),symfony/messenger(async Events API, rate-limit retries),symfony/notifier(chat transport)
Configuration
Every key, its default and its validation: docs/CONFIGURATION.md. A typical single-workspace setup:
# config/packages/framework.yaml (production): a dedicated lock resource, the app's own locks unchanged framework: lock: resources: default: ['%env(LOCK_DSN)%'] slack_kit: ['%env(SLACK_KIT_LOCK_DSN)%'] # redis://β¦ or a DBAL / PDO DSN (no +advisory) # config/packages/nowo_slack_kit.yaml nowo_slack_kit: credentials: store: instance_settings # bot token + signing secret in the encrypted "slack" section ingress: events_enabled: true # app_uninstalled / tokens_revoked replay_lock_factory: lock.slack_kit.factory # atomic replay memory on the dedicated resource interactions: value_ttl: P30D previous_secrets: [] # '%env(OLD_APP_SECRET)%' after a kernel.secret rotation linking: enabled: true identity_store: App\Slack\UserSlackIdentityStore account_resolver: App\Slack\CurrentUserAccountResolver
Interim decisions shared with symfony-beacon
These match symfony-beacon 1.122.1, so its adoption of the bundle changes nothing for buttons already posted:
- Link-confirm values may omit the nonce
n(Beacon has no nonce store). The bundle binds each confirmation to the claim withc(claim time). Signed confirmations withoutcare accepted only whileinteractions.legacy_untilis in the future; that leaves a bounded replay window (same account, same Slack id, at most 24 h) described in docs/SECURITY.md. t(workspace) is in a signed value only when the team id is known, and omitted otherwise.- Final keys
nowo_slack_kit.interactions.{legacy_until, previous_secrets, value_ttl}.legacy_untilis a strict UTC instant (2026-12-05T00:00:00Z).
The REQ-SEC-004 audit (appendix FS) fixes are listed in docs/CHANGELOG.md.
Details and the remaining differences from contract v1.1: docs/CHANGELOG.md.
Demo
demo/symfony8β Symfony 8.1 on FrankenPHP (worker mode) with MySQL; host port 8086. The Slack settings section, the account link card, signed buttons you can "click" through the real ingress, and an in-app fake Slack that answers the Web API and records every call. Nothing leaves the container.
make -C demo up-symfony8 # http://localhost:8086 make test-e2e # Playwright e2e against the running demo make demo-smoke # boot, HTTP 200, down (REQ-TEST-011)
Development
make up
make install
make test
make cs-check
make phpstan
make igor
make test-coverage-100
make validate-translations
make release-check
Documentation
- Installation
- Configuration
- Usage
- Templates
- Security
- Upgrading
- Changelog
- Moving an in-app Slack integration onto SlackKit
- Contributing
- Code of Conduct
- Release
- Engram
- Spec-driven development
- GitHub Spec Kit
Additional documentation
- Architecture
- PSR evaluation (REQ-CS-007)
- Demo (FrankenPHP)
- FrankenPHP worker audit (kernel not reset)
- GitHub Actions CI requirements
Tests and coverage
- Tests: PHPUnit β unit tests (client on
MockHttpClientfor every error row, signature verifier, replay guard, ingress order of checks,nsk1codec with the contract test vectors and negative checks, legacy policy, router, actor resolver, account linker transitions and limiters, secrets and redaction, Block Kit limits, stores, DI and build-time refusals, Notifier transport, Messenger middleware) and kernel integration tests (settings section with CSRF and Test connection, account card end to end with a signed click on the ingress, ingress routes); Playwright e2e on the demo. - PHP: coverage gate in CI (
composer test-coverage-100), target 100 %. - Fakes only: bot token
xoxb-test-not-a-real-token, teamT00000000, usersU00000001β¦, e-mails@example.test. - TS/JS: N/A Β· Python: N/A
License and author
MIT Β· Nowo.tech Β· HΓ©ctor Franco Aceituno

