Compare commits

...
Author SHA1 Message Date
james.maes 312e95cc17 Merge pull request 'fix(connect): a Continue to Slack link, a landing page that exists and a plain refusal' (#37) from fix/connect-continue-to-slack into develop
build / maven (push) Successful in 1m26s
maven / build (push) Successful in 1m26s
2026-11-11 12:32:16 +00:00
james.maesandClaude Fable 5.2 80bea0f931 fix(connect): a Continue to Slack link, a landing page that exists and a plain refusal
build / maven (push) Successful in 1m35s
maven / build (push) Successful in 1m35s
Install for my organization and Connect Slack on the Connections table
ended on an empty screen with only a Return button. The screen declared
a view field (authorizeUrl) but no component, and a frontend step shows
only the components it declares. The screen is now an HTML component:
one sentence about what happens, a Continue to Slack link to the consent
page (same tab, so Slack can return the browser) and the lifetime of the
link, read from SlackOAuthState.LIFETIME_SECONDS (10 minutes).

After Slack, the callback redirected to /preferences?section=slack,
which the host UI does not serve. It returns to
/app/preferences?section=slack with &result=connected or &error=<code>.

Connecting in an environment without the public URL or the Slack app
secrets failed with "IllegalArgumentException (Slack public URL is
required)" or "Slack secret unavailable". It is refused before any state
is issued with a user-facing message that says Slack is not set up in
this environment, which of the two is missing, and the names of the
missing environment variables, never a value.

The Preferences action still answers with the authorize address as the
whole message: core has no redirect field on an action result.
qqq-frontend-next opens a message that is only an https address.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 06:29:49 -06:00
james.maes 2508c6b93c Merge pull request 'build(deps): align with Gitea registry (parent 2.2.1, QQQ 4.1.0-RC.26)' (#34) from deps/align-gitea into develop
build / maven (push) Successful in 1m49s
maven / build (push) Successful in 1m49s
2026-11-07 19:50:51 +00:00
James MaesandClaude Opus 5.5 65f256351c refactor(delivery): adopt the core delivery SPI and classified values
build / maven (push) Successful in 1m13s
maven / build (push) Successful in 1m13s
Replace the qqq#1205 STAND-IN copy of the delivery SPI with the QQQ 4.1
core types (com.kingsrook.qqq.backend.core.delivery). The producer now
registers SlackDeliveryDestination as "slack" in the core
DeliveryDestinationsMetaData registry instead of deferring host
registration; a new test proves registration is idempotent and that core
loads the implementation from the registry.

Replace the qqq#1142 STAND-IN in SlackPayloadGuard: field classification
comes from core ClassifiedValues (honoring a host audit-value protector)
and tokenized restricted values use core ClassifiedValueTokens.changed().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 13:49:04 -06:00
james.maesandClaude Opus 5.5 cd3205e249 build(deps): align with Gitea registry (parent 2.2.1, QQQ 4.1.0-RC.26)
build / maven (push) Failing after 4m10s
maven / build (push) Failing after 4m10s
pom.xml: parent -> 2.2.1
pom.xml: qqq.snapshot.version: 4.1.0-RC.7 -> 4.1.0-RC.26
pom.xml: qqq-frontend-next: 1.0.0-RC.19 -> 1.0.0-RC.38
pom.xml: added qqq.version=4.1.0-RC.26 (parent BOM import)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 18:09:23 +00:00
james.maes 26aac94b52 Merge pull request 'ci: pin shared workflow to v1' (#33) from ci/pin-v1-develop into develop
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (push) Successful in 13s
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 10s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 13s
build / maven (push) Successful in 4m43s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
maven / build (push) Successful in 4m42s
2026-11-06 16:10:48 +00:00
james.maesandClaude Opus 5.5 7804c6aed7 ci: pin shared workflow to v1
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 8s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 7s
build / maven (push) Successful in 4m2s
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 6s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
maven / build (push) Successful in 4m1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:13:59 +00:00
james.maes e1f362b22c Merge pull request 'ci: use shared ci-workflows Maven workflow' (#32) from ci/ci-workflows into develop
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 5s
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / ci-result (push) Successful in 4s
build / maven (push) Successful in 2m47s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow d20a578c7d818bc7ca32a2ac2c675da4e4f27385] (push) Successful in 5s
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
maven / build (push) Successful in 2m47s
2026-11-05 23:18:12 +00:00
CI bf57535353 ci: rerun shared workflow
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1b3648d9421543350067e95e54959dfb471b26d5] (pull_request) Failing after 3s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 1s
build / maven (push) Successful in 10m46s
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Failing after 1s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
maven / build (push) Successful in 10m46s
2026-11-05 16:27:14 -06:00
james.maesandClaude Opus 5.5 48a2387ac6 ci: use shared ci-workflows Maven workflow
build / maven (push) Failing after 1s
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1b3648d9421543350067e95e54959dfb471b26d5] (pull_request) Failing after 1s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 7s
maven / build (push) Failing after 2s
QRun-IO/ci-policy: native / native-result (pull_request) Failing after 1s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
Replace the platform-ci release callers with one caller of
KofTwentyTwo/ci-workflows maven.yaml (qqq-orb port).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 22:08:36 +00:00
forge-recovery da103703aa ci: enroll shared Gitea lifecycle
QRun-IO/ci-policy: ci / ci-result (push) Successful in 4s
qrun-release / postpublish (push) Failing after 10m18s
Shared Java Maven CI / verify (push) Successful in 2m28s
QRun-IO/ci-policy: Shared Java Maven CI / verify (push) Successful in 1m52s
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
Shared Java Maven CI / secrets (push) Successful in 17s
QRun-IO/ci-policy: native / native-result (push) Successful in 6s
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
Shared Java Maven CI / gate (push) Successful in 12s
qrun-release / release (push) Successful in 33m18s
QRun-IO/ci-policy: Shared Java Maven CI / gate (push) Successful in 13s
Shared QRun source version / version (push) Failing after 10m18s
QRun-IO/ci-policy: ci / java-maven-6 (push) Successful in 33m29s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / secrets (push) Successful in 31s
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
2026-11-02 22:23:52 +00:00
forge-recovery 6d4309d5a7 ci: enroll shared Gitea lifecycle
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 10s
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / verify (push) Successful in 2m14s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 5s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / secrets (push) Successful in 29s
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / gate (push) Successful in 18s
QRun-IO/ci-policy: ci / java-maven-6 (push) Successful in 20m31s
2026-11-02 21:34:30 +00:00
forge-recoveryandCodex d74e6a1a71 ci: enroll shared Gitea release-candidate lifecycle
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / secrets (pull_request) Successful in 18s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / verify (pull_request) Successful in 2m45s
QRun-IO/ci-policy: Shared Java Maven CI / gate (pull_request) Successful in 17s
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Successful in 1h6m11s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 6s
Release candidate / release-rc (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 6s
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 20:21:07 +00:00
c4312ea7a1 ci: add the isolated postpublish job to the QRun release caller
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
Shared Java Maven CI / secrets (push) Successful in 17s
QRun-IO/ci-policy: Shared Java Maven CI / secrets (pull_request) Successful in 14s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
Shared Java Maven CI / verify (push) Canceled after 0s
qrun-release / postpublish (push) Canceled after 0s
QRun-IO/ci-policy: Shared Java Maven CI / verify (pull_request) Successful in 2m11s
QRun-IO/ci-policy: Shared Java Maven CI / gate (pull_request) Successful in 10s
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Successful in 3h5m7s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 5s
Release candidate / release-rc (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 4s
Shared Java Maven CI / gate (push) Canceled after 0s
qrun-release / release (push) Canceled after 2h26m33s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 15:53:53 +00:00
a5bae34659 ci: move the QRun release caller to the thin v2 contract
QRun-IO/ci-policy: Shared Java Maven CI / verify (pull_request) Successful in 2m23s
QRun-IO/ci-policy: Shared Java Maven CI / gate (pull_request) Canceled after 0s
Shared Java Maven CI / verify (push) Canceled after 0s
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-2 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-9 (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 4s
Shared Java Maven CI / secrets (push) Successful in 15s
QRun-IO/ci-policy: Shared Java Maven CI / secrets (pull_request) Successful in 18s
QRun-IO/ci-policy: ci / java-maven-4 (pull_request) Canceled after 20m46s
Shared Java Maven CI / gate (push) Canceled after 0s
qrun-release / release (push) Canceled after 20m49s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 5s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 06:21:08 +00:00
forge-recoveryandCodex 0a03bfb501 ci: add the identical QRun release caller
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 1s
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-2 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-7 (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / verify (pull_request) Failing after 1s
QRun-IO/ci-policy: Shared Java Maven CI / secrets (pull_request) Failing after 1s
QRun-IO/ci-policy: Shared Java Maven CI / gate (pull_request) Failing after 0s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-4 (pull_request) Failing after 1m32s
Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 04:15:28 +00:00
24 changed files with 461 additions and 1730 deletions

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
name: build
on:
push:
jobs:
maven:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
secrets: inherit
+7
View File
@@ -6,7 +6,14 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
- `SlackDeliveryDestination` now implements the QQQ 4.1 core delivery SPI (`com.kingsrook.qqq.backend.core.delivery`, qqq#1205) and the producer registers it as `slack` in the core `DeliveryDestinationsMetaData` registry; the stand-in SPI copy is removed.
- `SlackPayloadGuard` uses core `ClassifiedValues` for field classification and `ClassifiedValueTokens.changed()` for tokenized restricted values (qqq#1142), replacing its stand-in logic.
### Fixed
- **Install for my organization** and **Connect Slack** on the Connections table ended on an empty screen with only a Return button: the screen declared a view field but no component, and a frontend step shows only its components. The screen is now an HTML component with one sentence about what happens, a **Continue to Slack** link to the consent page (same tab) and the link's lifetime (10 minutes, `SlackOAuthState.LIFETIME_SECONDS`).
- After Slack, the callback redirected to `/preferences?section=slack`, which the host UI does not serve. It now returns to `/app/preferences?section=slack` with `&result=connected` or `&error=<code>`.
- Connecting in an environment without the public URL or the Slack app secrets failed with `IllegalArgumentException (Slack public URL is required)` or `Slack secret unavailable`. It is now refused with a user-facing message: "Slack is not set up in this environment", which of the two is missing, and the names of the missing environment variables, never a value.
- QBit metadata now reports the Maven artifact version, including release versions, from a filtered properties resource.
### Added
+16 -5
View File
@@ -121,7 +121,12 @@ There is one row per QQQ user and workspace row, unique (`userId`, `workspaceId`
1. **Start** (UI API only). It's a preference-section action, **Connect Slack**, in the user's Slack
section (core #1162), or the same action on the Connections table.
- It returns the Slack authorize URL.
- It returns the Slack authorize URL. Nobody copies it: the preference action answers with the address
alone, which the UI opens (see Preferences below), and the Connections table action ends on a screen
with one sentence, a **Continue to Slack** link and the link's lifetime.
- An environment without the public URL or without the Slack app secrets is refused before any state is
issued: "Slack is not set up in this environment", which of the two is missing, and the names of the
missing environment variables (never a value).
- The `state` is a server-signed, single-use token over the user id, tenant key, nonce, mode
(`user` or `user+bot`) and a 10-minute expiry.
- Users who hold `slack.workspace.install` in their tenant may also request the bot scopes.
@@ -132,8 +137,11 @@ There is one row per QQQ user and workspace row, unique (`userId`, `workspaceId`
environment variables.
- It upserts the tenant's `slack_workspace` (bot token, when granted) and the user's `slack_connection`
(user token and Slack user id).
- Then it redirects to the Preferences page with a result notice.
- Errors redirect with a plain message, never token text.
- Then it redirects to the Slack section of the Preferences page,
`/app/preferences?section=slack&result=connected`, which says the connection succeeded.
- Errors redirect there with a fixed code, `&error=<code>` (`sign_in_required`, `not_permitted`,
`authorization_denied`, `exchange_failed`, `connect_failed`), never provider or token text. The page
shows the code as words.
3. **Disconnect** calls `auth.revoke` on the user token, then marks the row REVOKED and clears the token.
**Uninstall** (admin) calls `apps.uninstall` or revokes the bot token, then marks the workspace
UNINSTALLED.
@@ -540,8 +548,11 @@ qbit-slack's own Preferences section (core #1162) has:
The section requires `slack.connect`, appears in the user menu, and rejects PATCH requests.
Actions are filtered per session; test messages also require `slack.send`. RC.7 has no action-level
permission property, so the section returns a filtered metadata view without mutating its shared
registrations. Connect and Install return their authorize URL in `UserPreferenceActionResult.message`;
RC.7 has no external-link or redirect result field, so this does not automatically navigate the client.
registrations. Connect and Install return their authorize URL as the whole of
`UserPreferenceActionResult.message`: core has no external-link or redirect result field (checked through
QQQ 4.1.0-RC.31). qqq-frontend-next sends the browser to a message that is only an `https:` address
instead of showing it, so the message must stay the address and nothing else. A typed redirect field in
core would replace this convention.
Without qbit-alerts, callers use `SendMessageAction` (`slack`, `me` or `user:<id>`) directly.
+4 -3
View File
@@ -22,6 +22,7 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<properties>
<qqq.version>4.1.0-RC.26</qqq.version> <!-- feeds qbit-build-parent's qqq-bom-pom import -->
<revision>0.1.0-SNAPSHOT</revision>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
@@ -34,7 +35,7 @@
<parent>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-build-parent</artifactId>
<version>2.1.1</version>
<version>2.2.1</version>
</parent>
<groupId>com.kingsrook.qbits</groupId>
@@ -77,7 +78,7 @@
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-frontend-next</artifactId>
<version>1.0.0-RC.19</version>
<version>1.0.0-RC.38</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -241,7 +242,7 @@
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<qqq.snapshot.version>4.1.0-RC.7</qqq.snapshot.version>
<qqq.snapshot.version>4.1.0-RC.26</qqq.snapshot.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -179,7 +179,7 @@ public class SlackQBitProducer implements QBitMetaDataProducer<SlackQBitConfig>
output.add(new SlackOutboxMessagingProviderMetaData().withName(qBitConfig.getBridgeProviderName()));
output.addSelfToInstance(qInstance);
com.kingsrook.qbits.slack.delivery.SlackDeliveryDestination.register();
com.kingsrook.qbits.slack.delivery.SlackDeliveryDestination.register(qInstance, getQBitConfig());
qInstance.addApp(com.kingsrook.qbits.slack.app.SlackApp.app(qInstance.getTables()));
com.kingsrook.qbits.slack.app.SlackApp.widgets().forEach(qInstance::addWidget);
if(isPreferencesAvailable())
@@ -26,15 +26,9 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qbits.slack.SlackQBitConfig;
import com.kingsrook.qbits.slack.api.SlackApi;
import com.kingsrook.qbits.slack.api.SlackApiMessage;
import com.kingsrook.qbits.slack.delivery.standin.DeliveryDestinationContext; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
import com.kingsrook.qbits.slack.delivery.standin.DeliveryDestinationInterface; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
import com.kingsrook.qbits.slack.delivery.standin.DeliveryRequest; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
import com.kingsrook.qbits.slack.delivery.standin.DeliveryResult; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
import com.kingsrook.qbits.slack.delivery.standin.DeliveryTargetOption; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
import com.kingsrook.qbits.slack.delivery.standin.DeliveryTargetStep; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
import com.kingsrook.qbits.slack.delivery.standin.QDeliveryDestinationMetaData; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
import com.kingsrook.qbits.slack.metadata.SlackTables;
import com.kingsrook.qbits.slack.outbox.SlackSendRequest;
import com.kingsrook.qbits.slack.outbox.SlackSendResult;
@@ -45,45 +39,72 @@ import com.kingsrook.qbits.slack.support.SlackRuntime;
import com.kingsrook.qbits.slack.support.SlackStore;
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.delivery.DeliveryDestinationContext;
import com.kingsrook.qqq.backend.core.delivery.DeliveryDestinationInterface;
import com.kingsrook.qqq.backend.core.delivery.DeliveryDestinationsMetaData;
import com.kingsrook.qqq.backend.core.delivery.DeliveryRequest;
import com.kingsrook.qqq.backend.core.delivery.DeliveryResult;
import com.kingsrook.qqq.backend.core.delivery.DeliveryTargetOption;
import com.kingsrook.qqq.backend.core.delivery.DeliveryTargetStep;
import com.kingsrook.qqq.backend.core.delivery.QDeliveryDestinationMetaData;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
/*******************************************************************************
** Host-callable Slack output destination until core's delivery SPI is released.
** Targets are workspaceId and where (me or a channel address from SlackApi).
** Slack output destination for the QQQ core delivery SPI
** (DeliveryDestinationInterface). The producer registers it in the core
** DeliveryDestinationsMetaData registry, so hosts that install core delivery
** offer it in their pickers. Targets are workspaceId and where (me or a
** channel address from SlackApi).
*******************************************************************************/
public class SlackDeliveryDestination implements DeliveryDestinationInterface
{
public static final String NAME = "slack";
/*******************************************************************************
** Describe the policy hosts must apply before invoking this destination.
** Describe the policy core applies before invoking this destination, using
** the Slack config of the current instance.
*******************************************************************************/
public QDeliveryDestinationMetaData metadata()
{
return (new QDeliveryDestinationMetaData().withName("slack").withLabel("Slack")
return (metadata(SlackRuntime.config()));
}
/*******************************************************************************
** Describe the policy core applies before invoking this destination.
*******************************************************************************/
public static QDeliveryDestinationMetaData metadata(SlackQBitConfig config)
{
return (new QDeliveryDestinationMetaData().withName(NAME).withLabel("Slack")
.withPermissionName("slack.send").withCodeReference(new QCodeReference(SlackDeliveryDestination.class))
.withMaxClassification(DataClassification.CONFIDENTIAL)
.withAboveMaxClassification(QDeliveryDestinationMetaData.AboveMaxClassification.REFUSE)
.withMaxAttachmentBytes(SlackRuntime.config().getMaxAttachmentBytes()).withAllowBearerLinks(false));
.withMaxAttachmentBytes(config.getMaxAttachmentBytes()).withAllowBearerLinks(false));
}
/*******************************************************************************
** Detect the real SPI without registering an incompatible temporary instance.
** Register with the core delivery registry; production may run repeatedly,
** and a destination already registered under this name is kept.
*******************************************************************************/
public static void register()
public static void register(QInstance instance, SlackQBitConfig config)
{
try
DeliveryDestinationsMetaData registry = DeliveryDestinationsMetaData.ofOrWithNew(instance);
if(registry.getDestinations().stream().noneMatch(destination -> NAME.equals(destination.getName())))
{
Class.forName("com.kingsrook.qqq.backend.core.delivery.DeliveryDestinationsMetaData");
com.kingsrook.qqq.backend.core.logging.QLogger.getLogger(SlackDeliveryDestination.class)
.info("Slack delivery stand-in will be replaced after RC.8; host registration is deferred"); // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
}
catch(ClassNotFoundException e)
{
// RC.6 hosts use this destination directly.
registry.withDestination(metadata(config));
}
}
/*******************************************************************************
** Offer only active connections and membership-checked targets of this user.
*******************************************************************************/
@@ -1,193 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2026. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.slack.delivery.standin; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
// STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
/*******************************************************************************
** DeliveryAttachment.
*******************************************************************************/
public class DeliveryAttachment implements java.io.Serializable
{
private String fileName;
private String mimeType;
private Long size;
private String storageTableName;
private String storageReference;
/*******************************************************************************
** Get fileName.
*******************************************************************************/
public String getFileName()
{
return (fileName);
}
/*******************************************************************************
** Set fileName.
*******************************************************************************/
public void setFileName(String fileName)
{
this.fileName = fileName;
}
/*******************************************************************************
** With fileName.
*******************************************************************************/
public DeliveryAttachment withFileName(String fileName)
{
this.fileName = fileName;
return (this);
}
/*******************************************************************************
** Get mimeType.
*******************************************************************************/
public String getMimeType()
{
return (mimeType);
}
/*******************************************************************************
** Set mimeType.
*******************************************************************************/
public void setMimeType(String mimeType)
{
this.mimeType = mimeType;
}
/*******************************************************************************
** With mimeType.
*******************************************************************************/
public DeliveryAttachment withMimeType(String mimeType)
{
this.mimeType = mimeType;
return (this);
}
/*******************************************************************************
** Get size.
*******************************************************************************/
public Long getSize()
{
return (size);
}
/*******************************************************************************
** Set size.
*******************************************************************************/
public void setSize(Long size)
{
this.size = size;
}
/*******************************************************************************
** With size.
*******************************************************************************/
public DeliveryAttachment withSize(Long size)
{
this.size = size;
return (this);
}
/*******************************************************************************
** Get storageTableName.
*******************************************************************************/
public String getStorageTableName()
{
return (storageTableName);
}
/*******************************************************************************
** Set storageTableName.
*******************************************************************************/
public void setStorageTableName(String storageTableName)
{
this.storageTableName = storageTableName;
}
/*******************************************************************************
** With storageTableName.
*******************************************************************************/
public DeliveryAttachment withStorageTableName(String storageTableName)
{
this.storageTableName = storageTableName;
return (this);
}
/*******************************************************************************
** Get storageReference.
*******************************************************************************/
public String getStorageReference()
{
return (storageReference);
}
/*******************************************************************************
** Set storageReference.
*******************************************************************************/
public void setStorageReference(String storageReference)
{
this.storageReference = storageReference;
}
/*******************************************************************************
** With storageReference.
*******************************************************************************/
public DeliveryAttachment withStorageReference(String storageReference)
{
this.storageReference = storageReference;
return (this);
}
}
@@ -1,65 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2026. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.slack.delivery.standin; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
// STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
/*******************************************************************************
** DeliveryDestinationContext.
*******************************************************************************/
public class DeliveryDestinationContext implements java.io.Serializable
{
private QDeliveryDestinationMetaData destination;
/*******************************************************************************
** Get destination.
*******************************************************************************/
public QDeliveryDestinationMetaData getDestination()
{
return (destination);
}
/*******************************************************************************
** Set destination.
*******************************************************************************/
public void setDestination(QDeliveryDestinationMetaData destination)
{
this.destination = destination;
}
/*******************************************************************************
** With destination.
*******************************************************************************/
public DeliveryDestinationContext withDestination(QDeliveryDestinationMetaData destination)
{
this.destination = destination;
return (this);
}
}
@@ -1,69 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2026. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.slack.delivery.standin; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
// STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.exceptions.QException;
/*******************************************************************************
** Owner-scoped output delivery provider.
*******************************************************************************/
public interface DeliveryDestinationInterface
{
/*******************************************************************************
** Describe the next target in the current owner session.
*******************************************************************************/
DeliveryTargetStep describeTargets(DeliveryDestinationContext context, Map<String, Serializable> chosen) throws QException;
/*******************************************************************************
** Validate a complete target before sending.
*******************************************************************************/
default List<String> validate(DeliveryRequest request) throws QException
{
return (List.of());
}
/*******************************************************************************
** Deliver idempotently using the request key.
*******************************************************************************/
DeliveryResult deliver(DeliveryRequest request) throws QException;
/*******************************************************************************
** Reconcile a previous delivery when supported.
*******************************************************************************/
default Optional<DeliveryResult> lookup(String idempotencyKey) throws QException
{
return (Optional.empty());
}
}
@@ -1,453 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2026. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.slack.delivery.standin; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
// STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
/*******************************************************************************
** DeliveryRequest.
*******************************************************************************/
public class DeliveryRequest implements java.io.Serializable
{
private String destinationName;
private String ownerUserId;
private String sourceType;
private String sourceKey;
private String title;
private String summary;
private String link;
private String idempotencyKey;
private Map<String, Serializable> targetValues = Map.of();
private List<DeliveryAttachment> attachments = List.of();
private DataClassification contentClassification;
private List<String> restrictedFieldLabels = List.of();
private boolean leaveOutRestricted;
/*******************************************************************************
** Get destinationName.
*******************************************************************************/
public String getDestinationName()
{
return (destinationName);
}
/*******************************************************************************
** Set destinationName.
*******************************************************************************/
public void setDestinationName(String destinationName)
{
this.destinationName = destinationName;
}
/*******************************************************************************
** With destinationName.
*******************************************************************************/
public DeliveryRequest withDestinationName(String destinationName)
{
this.destinationName = destinationName;
return (this);
}
/*******************************************************************************
** Get ownerUserId.
*******************************************************************************/
public String getOwnerUserId()
{
return (ownerUserId);
}
/*******************************************************************************
** Set ownerUserId.
*******************************************************************************/
public void setOwnerUserId(String ownerUserId)
{
this.ownerUserId = ownerUserId;
}
/*******************************************************************************
** With ownerUserId.
*******************************************************************************/
public DeliveryRequest withOwnerUserId(String ownerUserId)
{
this.ownerUserId = ownerUserId;
return (this);
}
/*******************************************************************************
** Get sourceType.
*******************************************************************************/
public String getSourceType()
{
return (sourceType);
}
/*******************************************************************************
** Set sourceType.
*******************************************************************************/
public void setSourceType(String sourceType)
{
this.sourceType = sourceType;
}
/*******************************************************************************
** With sourceType.
*******************************************************************************/
public DeliveryRequest withSourceType(String sourceType)
{
this.sourceType = sourceType;
return (this);
}
/*******************************************************************************
** Get sourceKey.
*******************************************************************************/
public String getSourceKey()
{
return (sourceKey);
}
/*******************************************************************************
** Set sourceKey.
*******************************************************************************/
public void setSourceKey(String sourceKey)
{
this.sourceKey = sourceKey;
}
/*******************************************************************************
** With sourceKey.
*******************************************************************************/
public DeliveryRequest withSourceKey(String sourceKey)
{
this.sourceKey = sourceKey;
return (this);
}
/*******************************************************************************
** Get title.
*******************************************************************************/
public String getTitle()
{
return (title);
}
/*******************************************************************************
** Set title.
*******************************************************************************/
public void setTitle(String title)
{
this.title = title;
}
/*******************************************************************************
** With title.
*******************************************************************************/
public DeliveryRequest withTitle(String title)
{
this.title = title;
return (this);
}
/*******************************************************************************
** Get summary.
*******************************************************************************/
public String getSummary()
{
return (summary);
}
/*******************************************************************************
** Set summary.
*******************************************************************************/
public void setSummary(String summary)
{
this.summary = summary;
}
/*******************************************************************************
** With summary.
*******************************************************************************/
public DeliveryRequest withSummary(String summary)
{
this.summary = summary;
return (this);
}
/*******************************************************************************
** Get link.
*******************************************************************************/
public String getLink()
{
return (link);
}
/*******************************************************************************
** Set link.
*******************************************************************************/
public void setLink(String link)
{
this.link = link;
}
/*******************************************************************************
** With link.
*******************************************************************************/
public DeliveryRequest withLink(String link)
{
this.link = link;
return (this);
}
/*******************************************************************************
** Get idempotencyKey.
*******************************************************************************/
public String getIdempotencyKey()
{
return (idempotencyKey);
}
/*******************************************************************************
** Set idempotencyKey.
*******************************************************************************/
public void setIdempotencyKey(String idempotencyKey)
{
this.idempotencyKey = idempotencyKey;
}
/*******************************************************************************
** With idempotencyKey.
*******************************************************************************/
public DeliveryRequest withIdempotencyKey(String idempotencyKey)
{
this.idempotencyKey = idempotencyKey;
return (this);
}
/*******************************************************************************
** Get targetValues.
*******************************************************************************/
public Map<String, Serializable> getTargetValues()
{
return (targetValues);
}
/*******************************************************************************
** Set targetValues.
*******************************************************************************/
public void setTargetValues(Map<String, Serializable> targetValues)
{
this.targetValues = targetValues;
}
/*******************************************************************************
** With targetValues.
*******************************************************************************/
public DeliveryRequest withTargetValues(Map<String, Serializable> targetValues)
{
this.targetValues = targetValues;
return (this);
}
/*******************************************************************************
** Get attachments.
*******************************************************************************/
public List<DeliveryAttachment> getAttachments()
{
return (attachments);
}
/*******************************************************************************
** Set attachments.
*******************************************************************************/
public void setAttachments(List<DeliveryAttachment> attachments)
{
this.attachments = attachments;
}
/*******************************************************************************
** With attachments.
*******************************************************************************/
public DeliveryRequest withAttachments(List<DeliveryAttachment> attachments)
{
this.attachments = attachments;
return (this);
}
/*******************************************************************************
** Get contentClassification.
*******************************************************************************/
public DataClassification getContentClassification()
{
return (contentClassification);
}
/*******************************************************************************
** Set contentClassification.
*******************************************************************************/
public void setContentClassification(DataClassification contentClassification)
{
this.contentClassification = contentClassification;
}
/*******************************************************************************
** With contentClassification.
*******************************************************************************/
public DeliveryRequest withContentClassification(DataClassification contentClassification)
{
this.contentClassification = contentClassification;
return (this);
}
/*******************************************************************************
** Get restrictedFieldLabels.
*******************************************************************************/
public List<String> getRestrictedFieldLabels()
{
return (restrictedFieldLabels);
}
/*******************************************************************************
** Set restrictedFieldLabels.
*******************************************************************************/
public void setRestrictedFieldLabels(List<String> restrictedFieldLabels)
{
this.restrictedFieldLabels = restrictedFieldLabels;
}
/*******************************************************************************
** With restrictedFieldLabels.
*******************************************************************************/
public DeliveryRequest withRestrictedFieldLabels(List<String> restrictedFieldLabels)
{
this.restrictedFieldLabels = restrictedFieldLabels;
return (this);
}
/*******************************************************************************
** Get leaveOutRestricted.
*******************************************************************************/
public boolean getLeaveOutRestricted()
{
return (leaveOutRestricted);
}
/*******************************************************************************
** Set leaveOutRestricted.
*******************************************************************************/
public void setLeaveOutRestricted(boolean leaveOutRestricted)
{
this.leaveOutRestricted = leaveOutRestricted;
}
/*******************************************************************************
** With leaveOutRestricted.
*******************************************************************************/
public DeliveryRequest withLeaveOutRestricted(boolean leaveOutRestricted)
{
this.leaveOutRestricted = leaveOutRestricted;
return (this);
}
}
@@ -1,139 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2026. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.slack.delivery.standin; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
// STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
import java.util.List;
/*******************************************************************************
** DeliveryResult.
*******************************************************************************/
public class DeliveryResult implements java.io.Serializable
{
/*******************************************************************************
** Recorded delivery outcome.
*******************************************************************************/
// STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
public enum Status
{
DELIVERED, QUEUED, REFUSED, FAILED_RETRYABLE, FAILED, UNAUTHORIZED
}
private Status status;
private String externalReference;
private List<String> reasons = List.of();
/*******************************************************************************
** Get status.
*******************************************************************************/
public Status getStatus()
{
return (status);
}
/*******************************************************************************
** Set status.
*******************************************************************************/
public void setStatus(Status status)
{
this.status = status;
}
/*******************************************************************************
** With status.
*******************************************************************************/
public DeliveryResult withStatus(Status status)
{
this.status = status;
return (this);
}
/*******************************************************************************
** Get externalReference.
*******************************************************************************/
public String getExternalReference()
{
return (externalReference);
}
/*******************************************************************************
** Set externalReference.
*******************************************************************************/
public void setExternalReference(String externalReference)
{
this.externalReference = externalReference;
}
/*******************************************************************************
** With externalReference.
*******************************************************************************/
public DeliveryResult withExternalReference(String externalReference)
{
this.externalReference = externalReference;
return (this);
}
/*******************************************************************************
** Get reasons.
*******************************************************************************/
public List<String> getReasons()
{
return (reasons);
}
/*******************************************************************************
** Set reasons.
*******************************************************************************/
public void setReasons(List<String> reasons)
{
this.reasons = reasons;
}
/*******************************************************************************
** With reasons.
*******************************************************************************/
public DeliveryResult withReasons(List<String> reasons)
{
this.reasons = reasons;
return (this);
}
}
@@ -1,129 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2026. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.slack.delivery.standin; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
// STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
/*******************************************************************************
** DeliveryTargetOption.
*******************************************************************************/
public class DeliveryTargetOption implements java.io.Serializable
{
private String value;
private String label;
private String description;
/*******************************************************************************
** Get value.
*******************************************************************************/
public String getValue()
{
return (value);
}
/*******************************************************************************
** Set value.
*******************************************************************************/
public void setValue(String value)
{
this.value = value;
}
/*******************************************************************************
** With value.
*******************************************************************************/
public DeliveryTargetOption withValue(String value)
{
this.value = value;
return (this);
}
/*******************************************************************************
** Get label.
*******************************************************************************/
public String getLabel()
{
return (label);
}
/*******************************************************************************
** Set label.
*******************************************************************************/
public void setLabel(String label)
{
this.label = label;
}
/*******************************************************************************
** With label.
*******************************************************************************/
public DeliveryTargetOption withLabel(String label)
{
this.label = label;
return (this);
}
/*******************************************************************************
** Get description.
*******************************************************************************/
public String getDescription()
{
return (description);
}
/*******************************************************************************
** Set description.
*******************************************************************************/
public void setDescription(String description)
{
this.description = description;
}
/*******************************************************************************
** With description.
*******************************************************************************/
public DeliveryTargetOption withDescription(String description)
{
this.description = description;
return (this);
}
}
@@ -1,226 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2026. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.slack.delivery.standin; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
// STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
import java.util.List;
/*******************************************************************************
** DeliveryTargetStep.
*******************************************************************************/
public class DeliveryTargetStep implements java.io.Serializable
{
private String name;
private String label;
private List<DeliveryTargetOption> options = List.of();
private boolean freeText;
private boolean complete;
private String summary;
/*******************************************************************************
** Get name.
*******************************************************************************/
public String getName()
{
return (name);
}
/*******************************************************************************
** Set name.
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** With name.
*******************************************************************************/
public DeliveryTargetStep withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Get label.
*******************************************************************************/
public String getLabel()
{
return (label);
}
/*******************************************************************************
** Set label.
*******************************************************************************/
public void setLabel(String label)
{
this.label = label;
}
/*******************************************************************************
** With label.
*******************************************************************************/
public DeliveryTargetStep withLabel(String label)
{
this.label = label;
return (this);
}
/*******************************************************************************
** Get options.
*******************************************************************************/
public List<DeliveryTargetOption> getOptions()
{
return (options);
}
/*******************************************************************************
** Set options.
*******************************************************************************/
public void setOptions(List<DeliveryTargetOption> options)
{
this.options = options;
}
/*******************************************************************************
** With options.
*******************************************************************************/
public DeliveryTargetStep withOptions(List<DeliveryTargetOption> options)
{
this.options = options;
return (this);
}
/*******************************************************************************
** Get freeText.
*******************************************************************************/
public boolean getFreeText()
{
return (freeText);
}
/*******************************************************************************
** Set freeText.
*******************************************************************************/
public void setFreeText(boolean freeText)
{
this.freeText = freeText;
}
/*******************************************************************************
** With freeText.
*******************************************************************************/
public DeliveryTargetStep withFreeText(boolean freeText)
{
this.freeText = freeText;
return (this);
}
/*******************************************************************************
** Get complete.
*******************************************************************************/
public boolean getComplete()
{
return (complete);
}
/*******************************************************************************
** Set complete.
*******************************************************************************/
public void setComplete(boolean complete)
{
this.complete = complete;
}
/*******************************************************************************
** With complete.
*******************************************************************************/
public DeliveryTargetStep withComplete(boolean complete)
{
this.complete = complete;
return (this);
}
/*******************************************************************************
** Get summary.
*******************************************************************************/
public String getSummary()
{
return (summary);
}
/*******************************************************************************
** Set summary.
*******************************************************************************/
public void setSummary(String summary)
{
this.summary = summary;
}
/*******************************************************************************
** With summary.
*******************************************************************************/
public DeliveryTargetStep withSummary(String summary)
{
this.summary = summary;
return (this);
}
}
@@ -1,396 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2026. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.slack.delivery.standin; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
// STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
** QDeliveryDestinationMetaData.
*******************************************************************************/
public class QDeliveryDestinationMetaData implements java.io.Serializable
{
/*******************************************************************************
** Policy for content above the destination ceiling.
*******************************************************************************/
// STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
public enum AboveMaxClassification
{
REFUSE, LINK_ONLY
}
private String name;
private String label;
private String icon;
private String permissionName;
private String emailProviderName;
private Integer order = 0;
private QCodeReference codeReference;
private DataClassification maxClassification = DataClassification.PUBLIC;
private Long maxAttachmentBytes;
private boolean allowBearerLinks;
private AboveMaxClassification aboveMaxClassification = AboveMaxClassification.REFUSE;
/*******************************************************************************
** Get name.
*******************************************************************************/
public String getName()
{
return (name);
}
/*******************************************************************************
** Set name.
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** With name.
*******************************************************************************/
public QDeliveryDestinationMetaData withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Get label.
*******************************************************************************/
public String getLabel()
{
return (label);
}
/*******************************************************************************
** Set label.
*******************************************************************************/
public void setLabel(String label)
{
this.label = label;
}
/*******************************************************************************
** With label.
*******************************************************************************/
public QDeliveryDestinationMetaData withLabel(String label)
{
this.label = label;
return (this);
}
/*******************************************************************************
** Get icon.
*******************************************************************************/
public String getIcon()
{
return (icon);
}
/*******************************************************************************
** Set icon.
*******************************************************************************/
public void setIcon(String icon)
{
this.icon = icon;
}
/*******************************************************************************
** With icon.
*******************************************************************************/
public QDeliveryDestinationMetaData withIcon(String icon)
{
this.icon = icon;
return (this);
}
/*******************************************************************************
** Get permissionName.
*******************************************************************************/
public String getPermissionName()
{
return (permissionName);
}
/*******************************************************************************
** Set permissionName.
*******************************************************************************/
public void setPermissionName(String permissionName)
{
this.permissionName = permissionName;
}
/*******************************************************************************
** With permissionName.
*******************************************************************************/
public QDeliveryDestinationMetaData withPermissionName(String permissionName)
{
this.permissionName = permissionName;
return (this);
}
/*******************************************************************************
** Get emailProviderName.
*******************************************************************************/
public String getEmailProviderName()
{
return (emailProviderName);
}
/*******************************************************************************
** Set emailProviderName.
*******************************************************************************/
public void setEmailProviderName(String emailProviderName)
{
this.emailProviderName = emailProviderName;
}
/*******************************************************************************
** With emailProviderName.
*******************************************************************************/
public QDeliveryDestinationMetaData withEmailProviderName(String emailProviderName)
{
this.emailProviderName = emailProviderName;
return (this);
}
/*******************************************************************************
** Get order.
*******************************************************************************/
public Integer getOrder()
{
return (order);
}
/*******************************************************************************
** Set order.
*******************************************************************************/
public void setOrder(Integer order)
{
this.order = order;
}
/*******************************************************************************
** With order.
*******************************************************************************/
public QDeliveryDestinationMetaData withOrder(Integer order)
{
this.order = order;
return (this);
}
/*******************************************************************************
** Get codeReference.
*******************************************************************************/
public QCodeReference getCodeReference()
{
return (codeReference);
}
/*******************************************************************************
** Set codeReference.
*******************************************************************************/
public void setCodeReference(QCodeReference codeReference)
{
this.codeReference = codeReference;
}
/*******************************************************************************
** With codeReference.
*******************************************************************************/
public QDeliveryDestinationMetaData withCodeReference(QCodeReference codeReference)
{
this.codeReference = codeReference;
return (this);
}
/*******************************************************************************
** Get maxClassification.
*******************************************************************************/
public DataClassification getMaxClassification()
{
return (maxClassification);
}
/*******************************************************************************
** Set maxClassification.
*******************************************************************************/
public void setMaxClassification(DataClassification maxClassification)
{
this.maxClassification = maxClassification;
}
/*******************************************************************************
** With maxClassification.
*******************************************************************************/
public QDeliveryDestinationMetaData withMaxClassification(DataClassification maxClassification)
{
this.maxClassification = maxClassification;
return (this);
}
/*******************************************************************************
** Get maxAttachmentBytes.
*******************************************************************************/
public Long getMaxAttachmentBytes()
{
return (maxAttachmentBytes);
}
/*******************************************************************************
** Set maxAttachmentBytes.
*******************************************************************************/
public void setMaxAttachmentBytes(Long maxAttachmentBytes)
{
this.maxAttachmentBytes = maxAttachmentBytes;
}
/*******************************************************************************
** With maxAttachmentBytes.
*******************************************************************************/
public QDeliveryDestinationMetaData withMaxAttachmentBytes(Long maxAttachmentBytes)
{
this.maxAttachmentBytes = maxAttachmentBytes;
return (this);
}
/*******************************************************************************
** Get allowBearerLinks.
*******************************************************************************/
public boolean getAllowBearerLinks()
{
return (allowBearerLinks);
}
/*******************************************************************************
** Set allowBearerLinks.
*******************************************************************************/
public void setAllowBearerLinks(boolean allowBearerLinks)
{
this.allowBearerLinks = allowBearerLinks;
}
/*******************************************************************************
** With allowBearerLinks.
*******************************************************************************/
public QDeliveryDestinationMetaData withAllowBearerLinks(boolean allowBearerLinks)
{
this.allowBearerLinks = allowBearerLinks;
return (this);
}
/*******************************************************************************
** Get aboveMaxClassification.
*******************************************************************************/
public AboveMaxClassification getAboveMaxClassification()
{
return (aboveMaxClassification);
}
/*******************************************************************************
** Set aboveMaxClassification.
*******************************************************************************/
public void setAboveMaxClassification(AboveMaxClassification aboveMaxClassification)
{
this.aboveMaxClassification = aboveMaxClassification;
}
/*******************************************************************************
** With aboveMaxClassification.
*******************************************************************************/
public QDeliveryDestinationMetaData withAboveMaxClassification(AboveMaxClassification aboveMaxClassification)
{
this.aboveMaxClassification = aboveMaxClassification;
return (this);
}
}
@@ -25,6 +25,7 @@ import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import com.kingsrook.qbits.slack.SlackQBitConfig;
import com.kingsrook.qbits.slack.metadata.SlackTables;
import com.kingsrook.qbits.slack.security.SlackSecrets;
@@ -35,6 +36,7 @@ import com.kingsrook.qbits.slack.support.SlackStore;
import com.kingsrook.qbits.slack.transport.SlackResponse;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.session.QSession;
@@ -59,8 +61,10 @@ public final class SlackConnect
/*******************************************************************************
** Start only for the signed-in user, never an automated or system session.
** An environment without a public URL or without the Slack app secrets is
** refused in words the user can act on, before any state is issued.
*******************************************************************************/
public static String startUrl(String userId, String mode)
public static String startUrl(String userId, String mode) throws QException
{
QSession session = session();
if(!session.getUser().getIdReference().equals(userId) || !session.hasPermission("slack.connect")
@@ -69,6 +73,11 @@ public final class SlackConnect
throw (new IllegalArgumentException("Slack connect is not permitted"));
}
SlackQBitConfig config = SlackRuntime.config();
String notSetUp = notSetUp(config);
if(notSetUp != null)
{
throw (new QUserFacingException(notSetUp));
}
String redirect = redirectUri(config);
String state = SlackOAuthState.issue(config, session, userId, tenant(config, session), mode);
return ("https://slack.com/oauth/v2/authorize?client_id=" + encode(SlackSecrets.get(config, config.getClientIdEnvName()))
@@ -274,14 +283,52 @@ public final class SlackConnect
return (session);
}
/*******************************************************************************
** Why Slack cannot be connected in this environment, or null when it can.
** Connecting needs two things from the host: the public URL Slack sends the
** browser back to, and the Slack app secrets (client id, client secret and
** token key). The sentence says which is missing and names the environment
** variables, never a value.
*******************************************************************************/
public static String notSetUp(SlackQBitConfig config)
{
boolean noUrl = config.getPublicBaseUrl() == null || config.getPublicBaseUrl().isBlank();
List<String> secrets = Stream.of(config.getClientIdEnvName(), config.getClientSecretEnvName(), config.getTokenKeyEnvName())
.filter(name -> !present(config, name)).toList();
if(!noUrl && secrets.isEmpty())
{
return (null);
}
String missing = noUrl && !secrets.isEmpty() ? "the public URL and the Slack app secrets are not configured"
: noUrl ? "the public URL is not configured" : "the Slack app secrets are not configured";
return ("Slack is not set up in this environment: " + missing
+ (secrets.isEmpty() ? "." : " (missing " + String.join(", ", secrets) + ")."));
}
/*******************************************************************************
** True when a named secret resolves to a value. The value is not kept.
*******************************************************************************/
private static boolean present(SlackQBitConfig config, String name)
{
try
{
SlackSecrets.get(config, name);
return (true);
}
catch(IllegalArgumentException e)
{
return (false);
}
}
/*******************************************************************************
** The exact redirect URI shared by start and exchange.
*******************************************************************************/
private static String redirectUri(SlackQBitConfig config)
{
if(config.getPublicBaseUrl() == null)
if(config.getPublicBaseUrl() == null || config.getPublicBaseUrl().isBlank())
{
throw (new IllegalArgumentException("Slack public URL is required"));
throw (new IllegalArgumentException("Slack is not set up in this environment: the public URL is not configured."));
}
return (config.getPublicBaseUrl().replaceAll("/+$", "") + config.getOauthCallbackPath());
}
@@ -37,6 +37,9 @@ import io.javalin.http.Context;
*******************************************************************************/
public class SlackOAuthRouteProvider implements QJavalinRouteProviderInterface
{
/** The Slack section of the host UI's Preferences page. */
public static final String RETURN_PATH = "/app/preferences?section=slack";
private QInstance instance;
/*******************************************************************************
@@ -137,7 +140,18 @@ public class SlackOAuthRouteProvider implements QJavalinRouteProviderInterface
QContext.init(previous);
}
context.header("Cache-Control", "no-store").header("Referrer-Policy", "no-referrer");
context.redirect(SlackRuntime.config(instance).getPublicBaseUrl().replaceAll("/+$", "")
+ "/preferences?section=slack" + (error == null ? "&result=connected" : "&error=" + error));
context.redirect(returnUrl(SlackRuntime.config(instance).getPublicBaseUrl(), error));
}
/*******************************************************************************
** Where the browser lands after Slack: the user's Slack preferences in the
** host UI (/app/preferences, like every other /app path this qbit links to),
** with a fixed outcome code the page turns into words. A host without a
** public URL gets a path on the same origin.
*******************************************************************************/
public static String returnUrl(String publicBaseUrl, String error)
{
return ((publicBaseUrl == null ? "" : publicBaseUrl.replaceAll("/+$", ""))
+ RETURN_PATH + (error == null ? "&result=connected" : "&error=" + error));
}
}
@@ -43,6 +43,9 @@ import org.json.JSONObject;
*******************************************************************************/
public final class SlackOAuthState
{
/** How long an issued state, and so the authorize address that carries it, stays valid. */
public static final int LIFETIME_SECONDS = 600;
private static final Map<String, Long> NONCES = new HashMap<>();
private static final int CAPACITY = 10_000;
@@ -72,7 +75,7 @@ public final class SlackOAuthState
throw (new IllegalArgumentException("Slack connect unavailable"));
}
String nonce = UUID.randomUUID().toString();
long expiry = now + 600;
long expiry = now + LIFETIME_SECONDS;
String payload = encode(new JSONObject().put("user", user).put("tenant", Objects.toString(tenant, ""))
.put("session", session.getUuid()).put("nonce", nonce).put("mode", mode).put("expiry", expiry).toString().getBytes(StandardCharsets.UTF_8));
String signed = payload + "." + encode(sign(config, payload));
@@ -27,15 +27,16 @@ import java.util.Map;
import java.util.regex.Pattern;
import com.kingsrook.qbits.slack.support.SlackRuntime;
import com.kingsrook.qbits.slack.template.VariableSchema;
import com.kingsrook.qqq.backend.core.actions.ai.AiDataClassifications;
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.ClassifiedValueTokens;
import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.ClassifiedValues;
import com.kingsrook.qqq.backend.core.context.QContext;
/*******************************************************************************
** Refuse restricted content before persistence. Uses RC.6 field classifications.
** STAND-IN(core=QRun-IO/qqq#1142, removeBy=4.1.0-RC.8): ClassifiedValues and
** ClassifiedValueTokens are absent in RC.6. Restricted tokens use changed:v1,
** a deliberately non-reversible marker, never a hash of a low-entropy value.
** Refuse restricted content before persistence. Field classification comes
** from core ClassifiedValues (which honors a host's audit-value protector);
** tokenized restricted values become core ClassifiedValueTokens.changed(), a
** deliberately non-reversible marker, never a hash of a low-entropy value.
*******************************************************************************/
public final class SlackPayloadGuard
{
@@ -88,7 +89,10 @@ public final class SlackPayloadGuard
{
throw (new IllegalArgumentException("Unknown classified field"));
}
classification = DataClassification.max(classification, AiDataClassifications.ofField(table, field));
if(ClassifiedValues.isClassified(table, field))
{
return (true);
}
}
return (classification != null && classification.isAtLeast(DataClassification.PII));
}
@@ -123,7 +127,7 @@ public final class SlackPayloadGuard
{
throw (new RestrictedVariableException(declaration.name()));
}
safe.put(entry.getKey(), "changed:v1");
safe.put(entry.getKey(), ClassifiedValueTokens.changed());
}
else
{
@@ -145,7 +149,7 @@ public final class SlackPayloadGuard
{
throw (new RestrictedVariableException(declaration.name()));
}
safe.put(declaration.name(), "changed:v1");
safe.put(declaration.name(), ClassifiedValueTokens.changed());
}
}
return (safe);
@@ -168,7 +172,7 @@ public final class SlackPayloadGuard
{
throw (new IllegalArgumentException("Restricted template variable"));
}
return ("changed:v1");
return (ClassifiedValueTokens.changed());
}
return (sanitize(classified.value(), tokenize));
}
@@ -92,7 +92,7 @@ public class SlackPreferences
/*******************************************************************************
** Start signed, single-use OAuth for the current user only.
*******************************************************************************/
public static String connect(boolean install)
public static String connect(boolean install) throws QException
{
return (SlackConnect.startUrl(owner(), install ? "user+bot" : "user"));
}
@@ -162,7 +162,11 @@ public class SlackPreferencesProvider implements UserPreferenceProviderInterface
}
/*******************************************************************************
** RC.7 has no redirect result; return the authorize URL in its message field.
** Core has no redirect field on an action result (checked through QQQ
** 4.1.0-RC.31), so Connect and Install answer with the authorize address as
** the whole message and nothing else. qqq-frontend-next sends the browser to
** a message that is only an https address instead of showing it; any other
** word in the message would turn it back into text to read.
** Row services retain ownership checks even after core has checked the row.
*******************************************************************************/
@Override
@@ -30,6 +30,7 @@ import com.kingsrook.qbits.slack.SlackQBitConfig;
import com.kingsrook.qbits.slack.delivery.SlackDispatcher;
import com.kingsrook.qbits.slack.metadata.SlackTables;
import com.kingsrook.qbits.slack.oauth.SlackConnect;
import com.kingsrook.qbits.slack.oauth.SlackOAuthState;
import com.kingsrook.qbits.slack.readiness.SlackReadiness;
import com.kingsrook.qbits.slack.template.SlackTemplates;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
@@ -42,6 +43,9 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractLoadStep;
@@ -67,6 +71,9 @@ public final class SlackProcesses
public static final String MAINTAIN = "slack_readiness";
public static final String DISPATCH = "slack_dispatch";
/** The one screen of Connect Slack and Install: its HTML is the process value "continue.html". */
public static final String CONTINUE_STEP = "continue";
/*******************************************************************************
@@ -110,31 +117,63 @@ public final class SlackProcesses
/*******************************************************************************
** OAuth starts without requiring an existing connection or selected record.
** The one screen is an HTML component: a frontend step shows only the
** components it declares, so a view field alone renders an empty screen.
*******************************************************************************/
private static QProcessMetaData connectAction(String name, String label)
{
return (new QProcessMetaData().withName(name).withLabel(label).withTableName(SlackTables.CONNECTION)
.withMinInputRecords(0).withPermissionRules(SlackTables.permissionRules())
.withStep(new QBackendStepMetaData().withName("authorize").withCode(new QCodeReference(ConnectStep.class)))
.withStep(new com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData().withName("result").withLabel("Connect Slack")
.withViewField(new com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData("authorizeUrl", com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType.STRING).withLabel("Open this Slack authorization URL"))));
.withStep(new QFrontendStepMetaData().withName(CONTINUE_STEP).withLabel("Continue to Slack")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.HTML))));
}
/*******************************************************************************
** Return the authorize URL through the standard process UI API.
** Put the way to Slack on the process screen through the standard UI API.
*******************************************************************************/
public static class ConnectStep implements BackendStep
{
/*******************************************************************************
** No remote mutation happens until the user approves at Slack.
** No remote mutation happens until the user approves at Slack. The screen
** gets a link, not an address to copy; the address itself stays available
** to API clients in the authorizeUrl value.
*******************************************************************************/
@Override
public void run(RunBackendStepInput input, RunBackendStepOutput output)
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
{
output.addValue("authorizeUrl", com.kingsrook.qbits.slack.preferences.SlackPreferences.connect(INSTALL.equals(input.getProcessName())));
boolean install = INSTALL.equals(input.getProcessName());
String url = com.kingsrook.qbits.slack.preferences.SlackPreferences.connect(install);
output.addValue("authorizeUrl", url);
output.addValue(CONTINUE_STEP + ".html", continueHtml(url, install));
}
}
/*******************************************************************************
** What the screen says: one sentence about what happens next, the link to
** Slack's consent page (same tab, so Slack can return the browser here), and
** when the link stops working. The link carries its own style because a host
** UI may render links in process HTML like the text around them.
*******************************************************************************/
static String continueHtml(String authorizeUrl, boolean install)
{
String paragraph = "<p style=\"margin:0 0 12px\">";
String what = install ? "Slack will ask you to approve installing this application for your organization and connecting your own Slack account."
: "Slack will ask you to approve connecting your Slack account to this application.";
return (paragraph + what + " Afterwards you return to your Slack preferences, which show the result.</p>"
+ paragraph + "<a href=\"" + escapeHtml(authorizeUrl) + "\" style=\"display:inline-block;padding:8px 16px;border-radius:6px;"
+ "background:#4A154B;color:#ffffff;font-weight:600;text-decoration:none\">Continue to Slack</a></p>"
+ "<p style=\"margin:0\">This link expires in " + SlackOAuthState.LIFETIME_SECONDS / 60 + " minutes. If it has expired, run this action again.</p>");
}
/*******************************************************************************
** Escape text for an HTML attribute or element.
*******************************************************************************/
private static String escapeHtml(String text)
{
return (text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;").replace("'", "&#39;"));
}
/*******************************************************************************
** The scheduler's entry point.
*******************************************************************************/
@@ -0,0 +1,222 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.slack;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import com.kingsrook.qbits.slack.oauth.SlackConnect;
import com.kingsrook.qbits.slack.oauth.SlackOAuthRouteProvider;
import com.kingsrook.qbits.slack.oauth.SlackOAuthState;
import com.kingsrook.qbits.slack.permissions.SlackPermissions;
import com.kingsrook.qbits.slack.processes.SlackProcesses;
import com.kingsrook.qqq.backend.core.actions.preferences.UserPreferencesAction;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/*******************************************************************************
** What a person sees when they start a Slack connection: a link to follow on
** the process screen, an address-only answer the Preferences panel can send
** the browser to, a landing page that exists, and a plain refusal when Slack
** is not set up in the environment.
*******************************************************************************/
class SlackConnectScreenTest
{
private static final String CLIENT_ID_CANARY = "client-id-canary-value";
/*******************************************************************************
** A configured environment: a public URL and every Slack app secret.
*******************************************************************************/
private static SlackTestEnv configured() throws Exception
{
return (SlackTestEnv.create(false, c -> c.withPublicBaseUrl("https://example.test")
.withSecretResolver(name -> "SLACK_TOKEN_KEY".equals(name) ? SlackTokenCipherTest.KEY_ONE : "fake-application-secret")));
}
/*******************************************************************************
** Connect and Install each end on a screen with content. A frontend step
** shows only its declared components, so a step without one is an empty
** screen with a Return button.
*******************************************************************************/
@Test
void connectAndInstallScreensDeclareWhatTheyShow() throws Exception
{
try(SlackTestEnv env = configured())
{
for(String name : List.of(SlackProcesses.CONNECT, SlackProcesses.INSTALL))
{
List<QFrontendStepMetaData> screens = env.instance().getProcess(name).getStepList().stream()
.filter(QFrontendStepMetaData.class::isInstance).map(QFrontendStepMetaData.class::cast).toList();
assertThat(screens).as(name).hasSize(1);
assertThat(screens.getFirst().getName()).isEqualTo(SlackProcesses.CONTINUE_STEP);
assertThat(screens.getFirst().getLabel()).isEqualTo("Continue to Slack");
assertThat(screens.getFirst().getComponents()).as(name + " screen components").isNotEmpty()
.extracting(QFrontendComponentMetaData::getType).containsExactly(QComponentType.HTML);
}
}
}
/*******************************************************************************
** The screen's HTML is a sentence, a link named Continue to Slack whose
** target is the authorize address, and the lifetime of the link read from
** the state. The address is in the link target only, never text to copy.
*******************************************************************************/
@Test
void theScreenLinksToSlackAndSaysWhenTheLinkExpires() throws Exception
{
try(SlackTestEnv env = configured())
{
env.asOwner("alice").withPermissions(SlackPermissions.CONNECT, SlackPermissions.INSTALL);
RunBackendStepOutput connect = new RunBackendStepOutput();
new SlackProcesses.ConnectStep().run(new RunBackendStepInput().withProcessName(SlackProcesses.CONNECT), connect);
String url = connect.getValueString("authorizeUrl");
String html = connect.getValueString(SlackProcesses.CONTINUE_STEP + ".html");
assertThat(url).startsWith("https://slack.com/oauth/v2/authorize?").contains("&state=");
assertThat(html).contains("<a href=\"" + url.replace("&", "&amp;") + "\"").contains(">Continue to Slack</a>");
assertThat(html).doesNotContain("target=");
assertThat(html).contains("approve connecting your Slack account");
assertThat(SlackOAuthState.LIFETIME_SECONDS).isEqualTo(600);
assertThat(html).contains("This link expires in 10 minutes.");
assertThat(html.replaceAll("<[^>]*>", " ")).as("visible text").doesNotContain("https://", "slack.com", "state=");
RunBackendStepOutput install = new RunBackendStepOutput();
new SlackProcesses.ConnectStep().run(new RunBackendStepInput().withProcessName(SlackProcesses.INSTALL), install);
assertThat(install.getValueString(SlackProcesses.CONTINUE_STEP + ".html"))
.contains("installing this application for your organization", ">Continue to Slack</a>", "scope=chat");
}
}
/*******************************************************************************
** Run as the UI runs it: the process stops on its one screen and the
** screen's HTML is among the values the UI API returns for it.
*******************************************************************************/
@Test
void installRunStopsOnTheContinueScreenWithItsHtml() throws Exception
{
try(SlackTestEnv env = configured())
{
env.asOwner("alice").setPermissions(Set.of(SlackPermissions.CONNECT, SlackPermissions.INSTALL));
RunProcessInput input = new RunProcessInput();
input.setProcessName(SlackProcesses.INSTALL);
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
RunProcessOutput output = new RunProcessAction().execute(input);
assertThat(output.getException()).isEmpty();
assertThat(output.getProcessState().getNextStepName()).hasValue(SlackProcesses.CONTINUE_STEP);
assertThat(String.valueOf(output.getValues().get(SlackProcesses.CONTINUE_STEP + ".html")))
.contains("<a href=\"https://slack.com/oauth/v2/authorize?", ">Continue to Slack</a>", "This link expires in 10 minutes.");
}
}
/*******************************************************************************
** The Preferences action answers with the address and nothing else: the UI
** follows a message that is only an https address, and shows any other.
*******************************************************************************/
@Test
void thePreferencesActionAnswersWithTheAddressAlone() throws Exception
{
try(SlackTestEnv env = configured())
{
env.asOwner("alice").setPermissions(Set.of(SlackPermissions.CONNECT, SlackPermissions.INSTALL));
for(String name : List.of(SlackProcesses.CONNECT, SlackProcesses.INSTALL))
{
String message = new UserPreferencesAction().runAction("slack", name, Map.of()).getMessage();
assertThat(message).as(name).startsWith("https://slack.com/oauth/v2/authorize?").doesNotContainAnyWhitespaces();
}
}
}
/*******************************************************************************
** After Slack, the browser lands on the Slack section of the host's
** Preferences page, which is under /app, with a fixed outcome code.
*******************************************************************************/
@Test
void theCallbackReturnsToTheSlackPreferencesPage()
{
assertThat(SlackOAuthRouteProvider.RETURN_PATH).isEqualTo("/app/preferences?section=slack");
assertThat(SlackOAuthRouteProvider.returnUrl("https://example.test/", null)).isEqualTo("https://example.test/app/preferences?section=slack&result=connected");
assertThat(SlackOAuthRouteProvider.returnUrl("https://example.test", "authorization_denied"))
.isEqualTo("https://example.test/app/preferences?section=slack&error=authorization_denied");
assertThat(SlackOAuthRouteProvider.returnUrl(null, "connect_failed")).isEqualTo("/app/preferences?section=slack&error=connect_failed");
}
/*******************************************************************************
** An environment that lacks the public URL, the Slack app secrets or both
** says so, says which, and names the missing variables, never a value.
*******************************************************************************/
@Test
void anEnvironmentWithoutSlackSaysWhatIsMissing() throws Exception
{
Function<String, String> noSecrets = name -> null;
Function<String, String> onlyClientId = name -> "SLACK_CLIENT_ID".equals(name) ? CLIENT_ID_CANARY : null;
Function<String, String> allSecrets = name -> "SLACK_TOKEN_KEY".equals(name) ? SlackTokenCipherTest.KEY_ONE : "fake-application-secret";
assertThat(refusal(null, allSecrets)).isEqualTo("Slack is not set up in this environment: the public URL is not configured.");
assertThat(refusal("https://example.test", noSecrets)).isEqualTo("Slack is not set up in this environment: "
+ "the Slack app secrets are not configured (missing SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_TOKEN_KEY).");
assertThat(refusal(null, onlyClientId)).isEqualTo("Slack is not set up in this environment: "
+ "the public URL and the Slack app secrets are not configured (missing SLACK_CLIENT_SECRET, SLACK_TOKEN_KEY).")
.doesNotContain(CLIENT_ID_CANARY);
}
/*******************************************************************************
** Start a connection every way a person can, in an environment built with
** the given public URL and secrets, and return the one refusal they all give.
** A user-facing exception is what the UI API shows as written.
*******************************************************************************/
private static String refusal(String publicBaseUrl, Function<String, String> secrets) throws Exception
{
try(SlackTestEnv env = SlackTestEnv.create(false, c -> c.withPublicBaseUrl(publicBaseUrl).withSecretResolver(secrets)))
{
env.asOwner("alice").setPermissions(Set.of(SlackPermissions.CONNECT));
String expected = SlackConnect.notSetUp(env.config());
assertThat(expected).isNotNull();
assertThatThrownBy(() -> SlackConnect.startUrl("alice", "user")).isInstanceOf(QUserFacingException.class).hasMessage(expected);
assertThatThrownBy(() -> new UserPreferencesAction().runAction("slack", SlackProcesses.CONNECT, Map.of()))
.isInstanceOf(QUserFacingException.class).hasMessage(expected);
assertThatThrownBy(() -> new SlackProcesses.ConnectStep().run(new RunBackendStepInput().withProcessName(SlackProcesses.CONNECT), new RunBackendStepOutput()))
.isInstanceOf(QUserFacingException.class).hasMessage(expected);
return (expected);
}
}
/*******************************************************************************
** A configured environment has nothing to refuse.
*******************************************************************************/
@Test
void aConfiguredEnvironmentIsNotRefused() throws Exception
{
try(SlackTestEnv env = configured())
{
assertThat(SlackConnect.notSetUp(env.config())).isNull();
}
}
}
@@ -25,16 +25,18 @@ import java.util.Map;
import com.kingsrook.qbits.slack.api.SlackApi;
import com.kingsrook.qbits.slack.delivery.SlackDeliveryDestination;
import com.kingsrook.qbits.slack.delivery.SlackDispatcher;
import com.kingsrook.qbits.slack.delivery.standin.DeliveryAttachment; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
import com.kingsrook.qbits.slack.delivery.standin.DeliveryDestinationContext; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
import com.kingsrook.qbits.slack.delivery.standin.DeliveryRequest; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
import com.kingsrook.qbits.slack.delivery.standin.DeliveryResult; // STAND-IN(core=QRun-IO/qqq#1205, removeBy=4.1.0-RC.8)
import com.kingsrook.qbits.slack.metadata.SlackTables;
import com.kingsrook.qbits.slack.security.SlackSecurity;
import com.kingsrook.qbits.slack.support.SlackStore;
import com.kingsrook.qbits.slack.template.SlackTemplates;
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
import com.kingsrook.qqq.backend.core.actions.tables.StorageAction;
import com.kingsrook.qqq.backend.core.delivery.DeliveryAttachment;
import com.kingsrook.qqq.backend.core.delivery.DeliveryDestinationContext;
import com.kingsrook.qqq.backend.core.delivery.DeliveryDestinations;
import com.kingsrook.qqq.backend.core.delivery.DeliveryDestinationsMetaData;
import com.kingsrook.qqq.backend.core.delivery.DeliveryRequest;
import com.kingsrook.qqq.backend.core.delivery.DeliveryResult;
import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
@@ -126,6 +128,25 @@ class SlackDeliveryDestinationTest
assertThat(context.getDestination().getMaxAttachmentBytes()).isEqualTo(env.config().getMaxAttachmentBytes());
}
/*******************************************************************************
** The producer registers Slack once in the core delivery registry, and core
** loads this implementation from it.
*******************************************************************************/
@Test
void registeredWithCoreDelivery() throws Exception
{
var registry = DeliveryDestinationsMetaData.of(env.instance());
assertThat(registry).isNotNull();
SlackDeliveryDestination.register(env.instance(), env.config());
var registered = registry.getDestinations().stream().filter(d -> SlackDeliveryDestination.NAME.equals(d.getName())).toList();
assertThat(registered).singleElement().satisfies(d ->
{
assertThat(d.getPermissionName()).isEqualTo("slack.send");
assertThat(d.getMaxAttachmentBytes()).isEqualTo(env.config().getMaxAttachmentBytes());
});
assertThat(DeliveryDestinations.implementation(registered.getFirst())).isInstanceOf(SlackDeliveryDestination.class);
}
/*******************************************************************************
** DM and channel use the delivery template and preserve the idempotency key.
*******************************************************************************/
@@ -98,12 +98,12 @@ class SlackOAuthRouteTest
URI uri = URI.create("http://127.0.0.1:" + app.port() + env.config().getOauthCallbackPath() + "?code=code&state=" + URLEncoder.encode(signed, StandardCharsets.UTF_8));
var unsigned = client.send(HttpRequest.newBuilder(uri).GET().build(), HttpResponse.BodyHandlers.ofString());
assertThat(unsigned.statusCode()).isEqualTo(302);
assertThat(unsigned.headers().firstValue("location").orElseThrow()).endsWith("&error=sign_in_required");
assertThat(unsigned.headers().firstValue("location").orElseThrow()).isEqualTo("https://example.test/app/preferences?section=slack&error=sign_in_required");
env.slack().script("oauth.v2.access", FakeSlackServer.Reply.of(200,
"{\"ok\":true,\"team\":{\"id\":\"T1\"},\"authed_user\":{\"id\":\"UA\",\"access_token\":\"xoxp-route-token\"}}"));
var request = HttpRequest.newBuilder(uri).header("Cookie", "sessionUUID=" + session.getUuid()).GET().build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
assertThat(response.headers().firstValue("location").orElseThrow()).isEqualTo("https://example.test/preferences?section=slack&result=connected");
assertThat(response.headers().firstValue("location").orElseThrow()).isEqualTo("https://example.test/app/preferences?section=slack&result=connected");
assertThat(response.headers().map().toString()).doesNotContain("xoxp-route-token", "fake-secret");
assertThat(client.send(request, HttpResponse.BodyHandlers.ofString()).headers().firstValue("location").orElseThrow()).endsWith("&error=connect_failed");
}