Conversation
Discovery's live-session presence needs to label participants by their member display name, but members are a workspace-owned aggregate. Adds findMemberDisplayName(orgId, userId) to the WorkspaceModuleApi named interface (reads public.members, no tenant schema required) so discovery resolves names through the ACL instead of reaching in.
STOMP session-lifecycle listeners run on the broker thread, which has no bound TenantContext. Persisting the verified orgId in the session attributes on CONNECT lets those listeners (discovery presence) resolve the tenant without re-parsing the JWT.
Introduces the SessionParticipant record and the SessionPresenceMessage snapshot (added to the sealed SessionRealtimeMessage hierarchy and the SessionEventType discriminator). The roster travels on the existing per-session topic so the client keeps a single subscription and switches on type, consistent with the other session events.
Adds an in-process presence layer for the discovery module: SessionPresenceRegistry keeps who is subscribed to each session topic, SessionPresenceTracker drives it from STOMP subscribe/unsubscribe/ disconnect events and rebroadcasts the roster snapshot, and SessionParticipantResolver labels participants via the workspace ACL with a Caffeine cache. No Redis: presence is per-connection JVM state, same single-instance trade-off as the SIMPLE broker.
Verifies join/leave semantics, multi-tab dedup (a user counts once and stays present until their last connection leaves), disconnect fan-out across sessions, and unknown-connection no-ops. Switches the inner presence map to a LinkedHashMap so the roster keeps a stable join order.
Adds a presence section to REALTIME.md (STOMP-event-driven, in-process registry, single-topic PRESENCE_STATE, multi-instance caveat) and a CHANGELOG entry under [Unreleased] for feature/discovery-presence.
Adds SessionParticipantResolverTest (name + deterministic avatar url, Caffeine caching hits the workspace once, generic fallback when the membership is unknown) and SessionPresenceTrackerTest (subscribe to a session topic broadcasts the roster, unrelated destinations are ignored, unsubscribe/disconnect rebroadcast an empty roster).
The Principal set via accessor.setUser() on the CONNECT frame does not carry over to later frames on the same STOMP session in practice — a SUBSCRIBE frame's accessor.getUser() comes back null, verified against a real running instance. Session attributes (already used for orgId) are the WebSocketSession's own attribute map and do persist across every frame, so the verified userId is now stashed there too.
SessionPresenceTracker relied on accessor.getUser() to identify the subscriber, which is null on SUBSCRIBE/UNSUBSCRIBE frames (see the StompAuthChannelInterceptor fix). This is why no presence roster ever appeared, even for a session's very own recorder. Reads userId from the same session-attribute mechanism already used for orgId instead, and adds a regression test pinning this exact failure mode. Verified end-to-end against a running instance: PRESENCE_STATE now broadcasts correctly with the resolved member name.
The doc claimed the CONNECT-bound Principal is available on later
frames ("so ... per-message security work"); empirically it is not.
Documents the actual mechanism (session attributes) so the next person
extending @MessageMapping or per-user routing doesn't rediscover this
the hard way.
Jackson's record serializer only emits canonical record components; type() was a plain interface-method override returning a hardcoded constant, not a component, so it was silently dropped from the JSON. The client's message.type === 'PRESENCE_STATE' switch therefore never matched even though the message itself arrived correctly over the WebSocket. Matches the existing @JsonProperty("type") idiom already used by SessionProcessingFailedMessage/SessionStoryGeneratedMessage/ SessionTranscriptSegmentMessage for the same fixed-type-constant case. Verified against a running instance: the wire payload now includes "type":"PRESENCE_STATE".
The existing presence tests all inspected the Java object directly via Mockito captors, so none of them would have caught the missing "type" JSON field — that requires a real Jackson pass over the actual wire output. Adds a parameterized test asserting every SessionRealtimeMessage implementation serializes a "type" matching type(), plus a reflection check that SessionPresenceMessage.type() stays @JsonProperty-annotated. Confirmed both fail against the pre-fix code (2 failures), pass after.
CI's "Integration tests" job was failing: RealtimeNotificationIntegrationTest and SuggestionBroadcastIntegrationTest each subscribe to a session topic and assume the first frame received is the one they triggered. Since presence now auto-broadcasts a PRESENCE_STATE message the instant any client subscribes to that same topic, that broadcast could land first and get misdeserialized into the test's expected type (its JSON still decodes cleanly — extra/missing fields are just ignored), racing the real message. Each subscribe() helper now filters frames by the expected SessionEventType before enqueuing, which is the correct fix given this codebase's existing single-topic/multiple-message-kinds design (client already switches on type) rather than a workaround.
feat(discovery): live session presence over STOMP
Now that api.tamci.app and app.tamci.app have real ACM certs wired into the ALB and CloudFront (reqsai-infra), the task definition should reference those instead of the raw ELB/CloudFront default hostnames.
fix: sync tamci.app domain URLs into develop
…tegration rbac permissions
…d jpa persistence
…ct, target and push
…push integration flow
Gmail requires the From header to match the authenticated account when no alias is configured. Reads the same secret key as MAIL_USERNAME instead of hardcoding an address, so rotating SMTP credentials never needs a code change.
…teway bounded context
…tions and V22 targets One table per migration script: V21 creates integration_connections (org-scoped credentials), V22 creates project_integration_targets (project-scoped push routing). No schema change — the combined V21 is split for clarity and ordering.
introduce CredentialType {API_TOKEN, OAUTH2} and extend IntegrationConnection
with cloudId + encrypted oauth refresh/access tokens and expiry, plus a factory
for oauth connections and a rotated-token updater. migration V23 adds the columns
additively (default API_TOKEN) and relaxes email/secret_ciphertext to nullable.
IntegrationConnectionResponse now carries credentialType and email may be null.
Replaces the hand-rolled async worker with two chunk-oriented batch jobs (jiraImportJob / jiraPushAllJob): step-scoped readers resolve the work list and plan the projection total, processors delegate per item to the existing import/push services, a fault-tolerant skip policy preserves the per-item failure semantics, and job/step listeners restore the tenant from job parameters, update the integration_sync_jobs projection and publish each snapshot over STOMP. The launcher adapter behind the IntegrationJobLauncher port captures the tenant into job parameters and starts executions asynchronously via the JobOperator.
…ints
POST import / push-all now validate the target, persist a RUNNING integration_sync_jobs row (409 INTEGRATION_JOB_ALREADY_RUNNING on a concurrent start) and return 202 with the IntegrationJobResponse snapshot; new GET jobs (?active=true) and GET jobs/{jobId} endpoints (INTEGRATION_READ) serve reload recovery. Removes the synchronous batch result DTOs.
Unit tests for the job counters/terminal transitions, the single-running-job 409 (pre-check and unique-index race), tenant capture into job parameters and restoration by the job listener, per-item progress accounting including skip-policy failures, and the import outcome mapping.
Extends ADR-0023 with a didactic section on the Spring Batch engine (job/instance/execution, chunk step, skip policy, public-schema metadata in the multi-tenant setup, projection-over-BATCH-tables rationale, tenant propagation), records the 202 job contract in the changelog and refreshes the Jira guide. Also removes a stray code fence at the ADR tail.
fix: sync MAIL_FROM fix into develop
DevTokenController mints a signed JWT for any user/org/role with no login (a bootstrap tool for exercising authenticated endpoints before the real login existed). It's @Profile("dev")-gated so its bean never registers elsewhere, but that annotation is invisible to Spring Security's filter chain — the /api/auth/** wildcard would have made the path fall through to permitAll regardless of profile if the bean ever did register outside dev (e.g. a profile misconfiguration in a shared environment). Adds an explicit, profile-aware rule evaluated before the wildcard: permitAll only when the dev profile is active, denyAll otherwise.
The STOMP realtime test filtered frames by message.type(), but every concrete SessionRealtimeMessage hardcodes type() as a fixed constant (it is a @JsonProperty getter, not a canonical record component). Force-casting any frame into the expected payload type therefore made type() lie: the automatic PRESENCE_STATE broadcast a client receives the instant it subscribes deserialized into e.g. SessionProcessingFailedMessage with type()==FAILED and a null reason, racing (and under CI load beating) the real message and failing the assertion. Read each frame as a raw Map, discriminate on the WIRE type discriminator, and convert only genuine matches (ignoring the type/presence-only fields that are not record components). Behaviour-neutral, test-only.
feat(gateway): Jira integration — OAuth 2.0 + API token, org connection & project story push
They required a JWT (never fully public), but any authenticated tenant user could reach them — more exposure than intended for internal architecture (modulith) and operational data (metrics). There's no platform-wide "ops" role in this codebase to gate them behind (authorization here is entirely tenant/org-scoped via @authz.*), and nothing consumes them today (no Prometheus/scraper, logs go to CloudWatch only; info returns nothing without management.info.* contributors configured). Not exposing them over HTTP at all is simpler and safer than inventing a role just for this. Only health stays exposed, still gated by show-details: when-authorized.
…oint fix: block /api/auth/dev-token outside the dev profile
JIRA_OAUTH_CALLBACK_URL derived from the same frontend domain as FRONTEND_URL (not a separate literal, can't drift out of sync). INTEGRATIONS_ENCRYPTION_KEY, JIRA_OAUTH_CLIENT_ID, JIRA_OAUTH_CLIENT_SECRET, and JIRA_OAUTH_STATE_SECRET read from the new reqsai/production/jira secret (reqsai-infra), matching the existing jwt/smtp/ai secrets pattern — the execution role's read permission for it was already granted via Terraform.
feat: wire Jira OAuth config into the ECS task definition
Matches the pattern already used by Stripe's checkout success/cancel URLs (application.yml) — JIRA_OAUTH_CALLBACK_URL becomes an optional override instead of a required literal, so it can no longer drift out of sync with FRONTEND_URL. Drops the now-redundant explicit value from the deployed task definition.
fix: default Jira OAuth callback URL to FRONTEND_URL
BILLING_PAYMENT_PROVIDER=stripe flips off the fake in-memory gateway. WEB_APP_URL (same value as FRONTEND_URL) feeds the checkout success/cancel URL derivation already in application.yml, so no explicit STRIPE_SUCCESS_URL/CANCEL_URL is needed. STRIPE_API_KEY and STRIPE_WEBHOOK_SECRET read from the new reqsai/production/stripe secret (reqsai-infra), matching the existing jwt/smtp/ai/jira pattern.
feat: wire Stripe billing config into the ECS task definition
…-unreleased docs: document Stripe billing and Jira callback default in CHANGELOG
5 tasks
…efinition-merge-conflict # Conflicts: # ecs/task-definition.json
16 tasks
…-merge-conflict fix: resolve ecs/task-definition.json merge conflict with main
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Merges
developintomainto ship everything since the last release cut. Billing/Stripe backend code (subscription lifecycle, token quota, payment gateway) was already onmainfrom an earlier merge — this release brings the pieces built since then:JIRA_OAUTH_CALLBACK_URLnow defaults to${FRONTEND_URL}/.../callbackinstead of requiring an explicit value.BILLING_PAYMENT_PROVIDER=stripe, price IDs, and the Stripe API key/webhook secret wired into the deployed task definition (currently ask_test_...key — swap forsk_live_...before relying on real charges)./api/auth/dev-tokenno longer reachable outside thedevprofile.[Unreleased]entries that were missing for the Stripe billing feature.ecs/task-definition.jsonchanges from the twohotfix/sync-*-developmerges (#59, #62) are already reflected onmain(they were develop catching up to hotfixes applied directly tomain) — no drift expected there.Test plan
build+verifyModularity) green on this PRdeploy.ymlregisters a new ECS task definition revision and the service comes up healthyhttps://api.tamci.app/actuator/healthstays healthy after rollout