Release staging to main#719
Conversation
* feat: ingest Gong customer call findings
* fix(gong): signature verification, TOCTOU race, empty-id guard, speaker fallback, RFC 7807 412
- Add per-integration sharedSecret generated at OAuth state-save time and
persisted through completeGongInstall into workspace_integration metadata.
GongIngestCall reads body bytes, verifies HMAC-SHA256 via X-Gong-Signature
header using verifyGongSignature (crypto/subtle.ConstantTimeCompare) before
any DB write.
- Fix resolveGongInstall to return a hard error (surfaced as 400) when
integrationID is empty, eliminating silent fallback to most-recent
integration.
- Fix TOCTOU race in createOrLinkGongFinding: extracted tryCreateGongFinding
with SELECT ... FOR UPDATE on the issue table, wrapped in a retry loop
(max 3 attempts) that retries on pgconn code 23505 unique violation.
- Fix gongExternalSpeaker: change fallback return from true to false so
internal members missing from the participants list are not mis-classified
as external/customer speakers.
- Fix GongConnect 412 response: replace raw map[string]string via problem.JSON
with problem.Write to emit RFC 7807 application/problem+json.
- Regenerate packages/sdk/src/generated.ts from packages/proto/openapi.yaml;
gong paths now appear in the TypeScript SDK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gong): move call ingestion webhook to public router
Gong's servers post to POST /gong/{integrationID}/calls with only an
X-Gong-Signature header — no session cookie or bearer token. The route
was previously mounted under the session-authenticated protected group,
so every inbound webhook received 401 before the HMAC check could run.
Move the route to the publicProvider group (same group as SlackEvents
and GitLabWebhook) so unauthenticated Gong requests reach the handler.
Remove the canManage/auth.FromContext guard from GongIngestCall (the
HMAC signature check is the correct auth boundary for this endpoint).
Add resolveGongWebhookInstall to look up the integration by ID alone,
mirroring the resolveGitLabWebhookInstall pattern.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: add zendesk ticket issue linking * fix(zendesk): address code review findings in ticket linking - Replace dynamic SQL string concatenation in resolveZendeskInstall with two separate static QueryRow calls (one for integrationID path, one for subdomain path) to eliminate SQL injection risk from Go string concat - Restore early-return guard in queueSentryAutomations when links slice is empty so the workflow_state category query no longer runs on every issue state-change with no Sentry/Zendesk links - Change ZendeskTicketCreate error response from 400 to 500 for internal server failures (DB errors, missing workflow state); 400 is reserved for input validation failures - Assign getZendeskSourceLink(event) to a const in issue-detail-view.tsx to avoid calling the function twice in the same render Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat: add Intercom conversation issue linking
* fix(intercom): address security and correctness issues from code review
- intercomSigningSecret: drop AUTH_INTERCOM_SECRET fallback; return ""
when INTERCOM_SIGNING_SECRET is absent so the 503 path fires correctly
instead of accepting requests signed with the OAuth client secret
- IntercomIssueUnlink: check RowsAffected() and return 404 when no row
matched, consistent with IntercomIssueLink behaviour
- intercomIssueDescriptionHTML: guard Permalink with HasPrefix("https://")
to block javascript: URI injection before building the <a href>
- IntercomConnect 412 path: replace problem.JSON map with problem.Write
for RFC 7807 consistency
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat: add Front conversation issue integration * fix(integrations): harden Front integration security and correctness - SSRF: replace unvalidated frontAPIBaseURL with normalizeFrontBaseURL that requires https and a .frontapp.com host on admin-supplied values - HTTP client timeout: pass 10s-timeout client to validateFrontToken instead of http.DefaultClient (no timeout) - Dead branch: failFrontJob now sets status "error" (not "degraded") on 401/403 permission failures so operators know the token needs rotation - FrontIssueCreate: distinguish validation sentinels (400) from DB/infra errors (500) via isFrontValidationError helper - frontIssueDescriptionHTML: build combined HTML first, call sanitizehtml.RichText once instead of twice Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: add salesforce case links * fix(salesforce): address SSRF, auth bypass, and double-call findings from code review - Validate fetchSalesforceUserInfo endpoint against salesforceOAuthBaseURL() before making the outbound HTTP request to prevent SSRF via a crafted token.ID in the OAuth response. - Remove the Authorization: Bearer fallback in salesforceSignedAction so HMAC signature verification is mandatory for every request; no static secret bypass is accepted. - Move HTTP status checks before json.Decode in exchangeSalesforceOAuth and fetchSalesforceUserInfo; include response body in error messages so non-2xx failures are surfaced accurately. - Assign getSalesforceSourceLink(event) to a const in the render path of issue-detail-view.tsx to avoid calling the function twice per render. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat: add Jira guided import slice
* fix(jira-import): address security and correctness issues from code review
- [CRITICAL] Fix silent import truncation: getJSON now decodes 2xx
responses directly via json.NewDecoder (no 4096 limit); limit kept
only on error branch for safe logging
- [CRITICAL] Fix JQL injection: validate projectKey against
^[A-Z][A-Z0-9_]{0,9}$ before use; wrap value in double-quoted JQL
- [HIGH] Require HTTPS in normalizeJiraBaseURL; reject http:// with a
clear error
- [HIGH] Fix N+1 query: fetch team key once before the issue import loop
instead of querying per issue via jiraTeamIdentifier
- [HIGH] Add pagination to issues(): startAt-based loop fetches all
pages; surface jiraTruncatedError warning when a hard cap truncates;
add Total/StartAt to jiraSearchResponse
- [MEDIUM] openapi.yaml: add writeOnly:true and description to token
field on WorkspaceImportExportActionRequest
- [LOW] web: change jiraBaseUrl initial state to "" (placeholder only)
- Update handler_test.go to expect quoted JQL and include total in mock
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(jira-import): make maxResults=0 fetch all pages; remove dead helper; add pagination test
- issues(): replace the maxResults<=0→100 reset with an `unlimited` bool.
When unlimited, only startAt>=response.Total or an empty page terminates
the loop — fetching 100-issue batches across as many pages as needed.
When a positive maxResults cap is supplied the hard-cap exit fires as
before and surfaces a jiraTruncatedError. The empty-page guard remains
so a misbehaving server can never cause an infinite loop.
- Remove dead jiraTeamIdentifier() — confirmed unreferenced after the N+1
fix that inlined the team-key lookup into importJiraIssues().
- Add TestJiraIssuesPaginatesAllPages: mock server returns total=150 across
two pages (100+50); asserts all 150 issues are returned and exactly 2
HTTP calls are made when maxResults=0.
- Add TestNormalizeJiraBaseURLRejectsHTTP: asserts http:// base URLs are
rejected with an HTTPS error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…jira-cloud-server-sync-plu
* feat: add Figma issue previews * fix(figma): code review fixes for PR #686 - Rename migration 0009_figma_sources.sql → 0010_figma_sources.sql to avoid collision with 0009_gitlab_merge_request_links.sql on staging - Change timestamp columns to timestamptz in figma_source table - Add format: uuid to the id path param on the refresh endpoint in OpenAPI - Update OpenAPI summary to be truthful: endpoint stamps refreshed_at only, does not call the Figma API - Regenerate SDK from updated OpenAPI spec - Fix RefreshFigmaSource auth check: use p, ok := auth.FromContext and return 401 if not ok (was silently discarding the ok bool) - Use SDK client (createBrowserApiClient) for handleFigmaRefresh instead of hardcoded fetch URL; remove local FigmaSource interface redeclaration and use components["schemas"]["FigmaSource"] from the SDK - Validate thumbnailUrl in FigmaPreviewCard: only render <img> when URL starts with https:// to prevent XSS via untrusted URLs - Update UI button label from "Refresh" to "Mark seen" to match reality Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat: enrich triage routing metadata * fix(triage): address code review correctness issues - [CRITICAL] applyTriageDecision no longer writes to http.ResponseWriter; server errors return (nil, 500) so BulkTriage can abort the batch safely before the response is sent, preventing double-write and nil-map dereference panics on mid-batch 500-path failures. DecideTriage checks status >= 500 and writes a generic title so DB internals are never surfaced in the response. - [HIGH] triageIssues now batch-loads labels (triageIssueLabelsBatch) and source context (triageIssueSourceContextBatch) with WHERE issue_id = ANY($1::uuid[]) instead of one query per issue, eliminating the N+1 on the triage list endpoint. - [HIGH] optionProjects adds "completed_at is null and canceled_at is null" to filter dead projects for both triage routing and issue creation — offering them as targets only produced validation errors. - [HIGH] settings_members.go (~253) now checks errors.As(*triageValidationError) before using the message as a 400 title; raw DB errors become 500s instead of leaking DB internals into the response. validate* helpers return *triageValidationError for user-visible messages. - [MEDIUM] triageSourceContext: renamed local var "context" to "out" to stop shadowing the "context" package import. - [LOW] validateTriageAssignee: added ::uuid cast on $2 for consistency with other UUID parameters in the package. Deferred (as noted in review): OpenAPI schema tightening MEDIUM, debounce/E2E LOWs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ntax - Remove incorrect jira/zendesk fallback config block: both configure via their own admin setup forms, not env var checks - Fix missing closing braces in handler_test.go caused by naive union merge of figma and intercom test blocks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Squash merge PR #711 after controller validation.
# Conflicts: # apps/api/internal/teams/handler_test.go # apps/api/internal/teams/triage.go
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1707d4e3f8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| func (h Handler) validateTriageAssignee(ctx context.Context, team triageTeam, userID string) error { | ||
| var ok bool | ||
| // $2::uuid cast added for consistency with other UUID parameters. | ||
| err := h.DB.QueryRow(ctx, `select exists(select 1 from team_member tm join member m on m.user_id=tm.user_id and m.workspace_id=$3::uuid where tm.team_id=$1::uuid and tm.user_id=$2::uuid)`, team.ID, userID, team.WorkspaceID).Scan(&ok) |
There was a problem hiding this comment.
Stop casting text user IDs to UUIDs
When a triage default or accept action includes an assignee whose user ID is not a UUID, this validation query fails before it can return a friendly validation result. The schema stores user.id/team_member.user_id as text and existing seeded/test users use values like demo-founder or random base64 strings, so selecting those users as triage assignees will hit invalid input syntax for type uuid instead of accepting the decision/settings.
Useful? React with 👍 / 👎.
| if stateChanged && (category == "completed" || category == "canceled") { | ||
| if err := h.queueZendeskAutomations(ctx, tx, workspaceID, after, category); err != nil { |
There was a problem hiding this comment.
Queue Zendesk automation outside the Sentry-only path
For an issue that is linked only to Zendesk, moving it to a completed/canceled state will never queue the Zendesk follow-up because this call is inside queueSentryAutomations after the Sentry-link query returns early when len(links) == 0. That means the new Zendesk writeback only runs for issues that also happen to have a Sentry link, which leaves normal Zendesk-only tickets unsynced.
Useful? React with 👍 / 👎.
| problem.Write(w, 500, "Update issue failed", err.Error()) | ||
| return | ||
| } | ||
| if err := h.queueFrontAutomations(r.Context(), tx, p.WorkspaceID, existing, updated); err != nil { |
There was a problem hiding this comment.
Avoid queuing Front writebacks twice
For Front-linked issues that transition to completed/canceled, this queues duplicate Front outbound jobs because queueProviderAutomations already calls queueFrontAutomations before returning. The second explicit call here inserts another identical front/outbound_delivery job in the same transaction, so the worker can reopen/comment on the same conversation twice for one issue update.
Useful? React with 👍 / 👎.
| required: [provider, name, description, id, status, displayName, externalId, connectedAt, setupRequirement, actions, health, details] | ||
| properties: | ||
| provider: { type: string, enum: [github, gitlab, jira, slack, sentry, zendesk, discord, microsoft_teams] } | ||
| provider: { type: string, enum: [github, gitlab, jira, slack, sentry, zendesk, discord, microsoft_teams, figma] } |
There was a problem hiding this comment.
Include all returned integration providers in the schema
The /integrations response now returns catalog entries for intercom, salesforce, gong, and front, but the Integration.provider enum still excludes those values. Generated clients and OpenAPI response validators will treat valid API responses containing these new providers as out-of-contract, even though the backend now emits them from the catalog.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d7021bdf40
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| func (h Handler) resolveIssueContext(ctx context.Context, p auth.Principal, identifier, teamKey string) (contextSnapshot, error) { | ||
| var snapshot contextSnapshot | ||
| var description, assignee, projectName, projectSlug sql.NullString | ||
| err := h.DB.QueryRow(ctx, `select i.id::text,i.identifier,i.title,coalesce(i.description,''),t.key,ws.name,i.priority::text,coalesce(u.name,u.email,''),p.name,p.slug from issue i join team t on t.id=i.team_id join workflow_state ws on ws.id=i.state_id join member m on m.workspace_id=t.workspace_id and m.user_id=$2 left join "user" u on u.id=i.assignee_id left join project p on p.id=i.project_id where t.workspace_id=$1::uuid and upper(i.identifier)=upper($3) and ($4='' or upper(t.key)=upper($4)) limit 1`, p.WorkspaceID, p.UserID, identifier, teamKey).Scan(&snapshot.ID, &snapshot.Identifier, &snapshot.Title, &description, &snapshot.TeamKey, &snapshot.State, &snapshot.Priority, &assignee, &projectName, &projectSlug) |
There was a problem hiding this comment.
Enforce private team access for agent contexts
When a non-admin workspace member who is not in a private team enters one of that team's issue identifiers, this query still returns the issue because it only checks workspace membership via member m and never gates t.is_private with team_member or an admin/owner role. The resulting agent snapshot/output includes the private issue title, description, assignee, and project, so creating an agent run leaks private-team issue data to any workspace member who can guess an identifier.
Useful? React with 👍 / 👎.
| } | ||
| } | ||
| var nextNumber int32 | ||
| if err := tx.QueryRow(ctx, `select coalesce(max(number),0)+1 from issue where team_id=$1::uuid for update`, teamID).Scan(&nextNumber); err != nil { |
There was a problem hiding this comment.
Avoid locking an aggregate when allocating Gong issue numbers
For any actionable Gong call that needs a new issue, PostgreSQL rejects FOR UPDATE on this aggregate max(number) query, so tryCreateGongFinding returns an error before inserting the issue and the webhook responds 500 instead of linking the finding. Lock a real row/table or use the existing issue-number allocation pattern without FOR UPDATE on the aggregate result.
Useful? React with 👍 / 👎.
Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: port google sheets scheduled sync * fix: record google sheets oauth failures * fix: trim google sheets worker whitespace --------- Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat: add guided GitHub import foundation * fix: use shared bool parser for GitHub imports --------- Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…in-713 # Conflicts: # apps/api/internal/comments/handler.go # apps/api/internal/integrations/handler.go # apps/api/internal/issues/handler.go # apps/api/internal/labels/handler.go # apps/web/src/app/(app)/settings/integrations/page.tsx # apps/web/src/components/issue-detail-view.tsx # apps/web/tests/integrations-view.test.tsx
16e0f9f to
1665cff
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1665cff3ce
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| updated_at timestamp NOT NULL DEFAULT now() | ||
| ); | ||
|
|
||
| ALTER TABLE customer ADD COLUMN IF NOT EXISTS domain text; |
There was a problem hiding this comment.
Relax legacy customer source columns in the migration
Because migration 0011 already creates customer/customer_request with source_provider and source_external_id as NOT NULL (plus customer_request.excerpt), these ADD COLUMN compatibility steps leave those legacy constraints in place on both fresh and upgraded databases. The new first-class handlers insert only domain/name/... and title/body/..., so creating a customer or request will fail with NOT NULL violations until this migration drops/defaults/backfills the old Gong-only columns.
Useful? React with 👍 / 👎.
| } | ||
|
|
||
| func linkedIssuesForRequest(ctx context.Context, db queryer, requestID string) ([]LinkedIssue, error) { | ||
| rows, err := db.Query(ctx, `select i.id::text, i.identifier, i.title, t.key from issue_customer_request icr join issue i on i.id=icr.issue_id join team t on t.id=i.team_id where icr.customer_request_id=$1::uuid order by i.updated_at desc`, requestID) |
There was a problem hiding this comment.
Filter private-team issue links from customer responses
When a customer request is linked to an issue in a private team, any non-guest workspace member can load the customer/request response and this query returns the private issue identifier, title, and team key without checking team_member membership or owner/admin role. Since allowCustomerRequests permits all members, customer request responses become a private-issue metadata leak unless linked issues are filtered with the same private-team access predicate used elsewhere.
Useful? React with 👍 / 👎.
Brings the released TTY terminal login redesign (#695) and the insights-overlay opacity fix into staging, plus the rest of current main. Conflict resolutions: - comments/issues/labels handlers: keep both realtime sync publish (#699) and outbound webhook enqueue (main). - integrations/handler.go: keep required (non-omitempty) details field and the HealthSummary row column; drop duplicate Metadata field. - teams/triage.go: keep staging's no-writer (map,int) decision contract and due_date support; graft in main's issueauthz relationship validation and drop the duplicate tx.Begin. - teams/handler_test.go: keep both added tests. - settings/integrations page: use the named IntegrationDetails type, extended with the Google Sheets fields. Merge fixups: - repoint 6 integration disconnect handlers from the removed revokeProvider to main's disconnectProvider (arg order swapped). Migration reconciliation: - allowlist the parallel-branch 0010/0011 duplicate prefixes in check-migrations.mjs (same convention used for 0006-0008). - add 0012_customer_requests_reconcile.sql to unify the customer / customer_request schema introduced independently by the Gong ingestion (0010) and the customer-requests CRM (0011): make Gong-only provider/excerpt columns nullable so CRM inserts succeed, and guarantee the superset columns and upsert indexes regardless of apply order. make check passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…in-713 # Conflicts: # apps/api/internal/comments/handler.go # apps/api/internal/integrations/handler.go # apps/api/internal/issues/handler.go # apps/api/internal/labels/handler.go # apps/api/internal/teams/triage.go # apps/web/src/app/(app)/settings/integrations/page.tsx
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0039dd8926
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| func (c githubAPIClient) fetchRepoIssues(ctx context.Context, fullName string) ([]githubAPIIssue, error) { | ||
| var out []githubAPIIssue | ||
| for page := 1; ; page++ { | ||
| path := "/repos/" + escapedGitHubRepo(fullName) + "/issues?state=all&per_page=100&page=" + strconv.Itoa(page) |
There was a problem hiding this comment.
Honor the open-only GitHub import scope
When the import dialog is left at its default open-only scope, this still requests state=all and then fetches comments for every closed issue before the later confirm step skips them. On repositories with a long closed-issue history, the guided import can be unnecessarily slow or hit GitHub rate limits even though the user did not ask to import closed issues; pass the normalized scope through to the issues request and use state=open unless closed issues are included.
Useful? React with 👍 / 👎.
| coalesce(workspace_integration_id::text,'') as integration_id, | ||
| created_at | ||
| from integration_thread_link | ||
| where issue_id=$1::uuid and provider in ('sentry','github','gong','front','salesforce','jira') and external_permalink is not null |
There was a problem hiding this comment.
Don’t emit empty integration IDs for GitHub sources
Including github here makes issue detail responses for GitHub-imported issues return integrationId: "", because the import path creates integration_thread_link rows without workspace_integration_id. That value violates the IssueExternalSource.integrationId UUID contract and can break generated clients/response validators for imported GitHub issues; either omit/nullable this field for unintegrated sources or persist a real integration id.
Useful? React with 👍 / 👎.
| const customerRes = await fetch("/api/customers", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| name: trimmedCustomerName, | ||
| domain: customerDomain.trim() || null, | ||
| source: "manual", | ||
| }), | ||
| }); | ||
| if (!customerRes.ok) { | ||
| throw new Error("Create customer failed"); |
There was a problem hiding this comment.
Reuse existing customers before creating requests
When adding feedback for a customer that already exists, this always posts to POST /api/customers first. If the same domain is entered the API returns a 409 and the request is never created, while entering the same customer without a domain creates duplicate customer records; the issue-side flow needs to select/find an existing customer or recover from the conflict before posting the new request.
Useful? React with 👍 / 👎.
…ity (#726) - fix hardcoded /home/jaeyunha path -> $HOME/wt (was broken on macOS) - fix npm install -> pnpm install (this is a pnpm monorepo) - add remote-branch support (origin/foo: fetch + upstream tracking) - flatten '/' in branch names for the worktree dir - resolve repo root via git rev-parse (works from any cwd) - copy .codex/.agents alongside .claude; clean up + bail on install failure - add EXPONENTIAL_WORKTREE_OVERRIDE_BASE override Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ee93849716
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return gongFindingResult{}, err | ||
| } | ||
| metadataRaw, _ := json.Marshal(map[string]any{"callId": call.ID, "findingId": finding.ID, "timestampMs": finding.TimestampMs, "speaker": finding.Speaker, "speakerEmail": finding.SpeakerEmail, "accountId": call.Account.ID, "participants": call.Participants}) | ||
| if _, err := tx.Exec(ctx, `insert into customer_request (workspace_id, customer_id, issue_id, source_provider, source_external_id, source_url, title, excerpt, metadata, created_at, updated_at) values ($1::uuid,$2::uuid,$3::uuid,'gong',$4,$5,$6,$7,$8::jsonb,now(),now()) on conflict (workspace_id, source_provider, source_external_id) do update set issue_id=excluded.issue_id, customer_id=excluded.customer_id, source_url=excluded.source_url, title=excluded.title, excerpt=excluded.excerpt, metadata=excluded.metadata, updated_at=now()`, install.WorkspaceID, customerID, issueID, finding.ID, finding.Permalink, finding.Title, finding.Excerpt, metadataRaw); err != nil { |
There was a problem hiding this comment.
Link Gong requests through the new join table
When an actionable Gong finding creates a customer request, this only writes the legacy customer_request.issue_id column. The new issue/customer request surfaces and filters introduced in this change read links exclusively from issue_customer_request, so Gong-created feedback will not appear on the issue detail, customer related-work counts, or customer request linked issues even though it was created for this issue. Return the request id from this upsert and insert the corresponding join row in the same transaction.
Useful? React with 👍 / 👎.
| domain=coalesce($4::text, domain), | ||
| revenue=coalesce($5::numeric, revenue), | ||
| size=coalesce($6::integer, size), | ||
| tier=coalesce($7::text, tier), | ||
| status=coalesce($8::text, status), | ||
| owner_id=coalesce($9::text, owner_id), | ||
| source=coalesce($10::text, source), |
There was a problem hiding this comment.
Honor explicit nulls when patching customers
When a client clears a nullable field by sending {"domain": null} (or tier, ownerId, source, etc.), this coalesce treats the explicit null the same as an omitted field and keeps the old value. The OpenAPI PATCH schema exposes these fields as nullable, so users cannot remove an incorrect domain/owner/source once set; track field presence separately and set SQL NULL for present-null values.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4a25219fd2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| )} | ||
| {isVisible("customers") && ( | ||
| <SidebarLink | ||
| href="/customers" |
There was a problem hiding this comment.
Route customer links through the workspace slug
This new sidebar item relies on withWorkspaceSlug, but customers is still classified as a public route rather than an app route in the workspace path helper, so the canonical href remains /customers. In a workspace session, clicking Customers opens the public marketing customers page instead of /{workspaceSlug}/customers (and the same helper also leaves direct customer app paths unnormalized), making the new workspace customer area unreachable from this navigation.
Useful? React with 👍 / 👎.
| join issue i on i.id=ztl.issue_id | ||
| join team t on t.id=i.team_id | ||
| join workflow_state ws on ws.id=i.state_id | ||
| where ztl.workspace_integration_id=$1::uuid and ztl.ticket_id=$2 and t.workspace_id=$3::uuid and i.archived_at is null |
There was a problem hiding this comment.
Filter private teams from Zendesk status results
When a ticket is linked before its team is made private, this status query still returns the issue identifier, title, and workflow state to the Zendesk action because it only checks workspace and archive status. The search/link paths in this same handler explicitly require coalesce(t.is_private,false)=false, so this endpoint becomes a private-issue metadata leak to Zendesk users who can view the linked ticket.
Useful? React with 👍 / 👎.
Focused controller QA passed: apps/api go test ./..., import/export Vitest 5/5, diff-check clean.
Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
…in-713 # Conflicts: # apps/web/tests/import-export-view.test.tsx
0fc011d to
9bea0f9
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9bea0f92f4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| func (h Handler) saveJiraIntegration(ctx context.Context, workspaceID string, userID string, credential jiraCredential, account jiraUser, projects []jiraProject) (string, error) { | ||
| metadataRaw, _ := json.Marshal(map[string]any{"deployment": credential.Deployment, "baseUrl": credential.BaseURL, "accountId": firstNonEmptyString(account.AccountID, account.Name), "accountName": firstNonEmptyString(account.DisplayName, account.EmailAddress, account.Name), "projectCount": len(projects)}) | ||
| credentialRaw, _ := json.Marshal(credential) |
There was a problem hiding this comment.
Encrypt Jira credentials before storing them
When an admin connects Jira, this stores the API token/email as raw JSON in provider_credential.encrypted_payload instead of using encryptedProviderCredentialJSON like the other providers in this change. That leaves Jira secrets readable from the database/backups and also bypasses the legacy backfill/decrypt path used elsewhere, so Jira credentials remain unprotected after setup.
Useful? React with 👍 / 👎.
| update customer_request set title=$3, body=coalesce($4, body), source=coalesce($5, source), source_url=coalesce($6, source_url), external_provider=coalesce($7, external_provider), external_id=coalesce($8, external_id), important=$9, updated_at=now() | ||
| where workspace_id=$1::uuid and id=$2::uuid`, p.WorkspaceID, existing.ID, title, cleanPtr(input.Body), cleanPtr(input.Source), cleanPtr(input.SourceURL), cleanPtr(input.ExternalProvider), cleanPtr(input.ExternalID), important) |
There was a problem hiding this comment.
Honor explicit nulls when patching requests
For PATCH requests that send nullable fields such as {"body": null} or {"sourceUrl": null}, this coalesce path treats the explicit null the same as an omitted field and keeps the old value. The customer-request PATCH contract exposes these fields as nullable, so users cannot clear an obsolete body/source/external link once it has been set; track field presence separately and write SQL NULL when a field is present-null.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a32ba03a33
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| case "add", "replace": | ||
| if err := h.addGroupMembers(r.Context(), workspaceID(r.Context()), role, members); err != nil { |
There was a problem hiding this comment.
Replace SCIM group membership on PATCH replace
When a SCIM client reconciles a role group with op: "replace" and a members list, this path only calls addGroupMembers, so users omitted from the replacement payload keep their previous role. For the admin group, removing someone in the IdP can leave them as an Exponential admin until a separate remove/PUT happens; use replaceGroupMembers for replace operations so omitted members are demoted.
Useful? React with 👍 / 👎.
| return | ||
| } | ||
| for _, op := range input.Operations { | ||
| members := patchMembers(op.Value) |
There was a problem hiding this comment.
Parse SCIM remove member path filters
For SCIM PATCH remove operations that identify the member in the path, e.g. path: "members[value eq \"<member-id>\"]" with no value, patchMembers(op.Value) returns an empty slice and removeGroupMembers becomes a no-op. IdPs commonly send removals in that shape, so users removed from admin/guest groups can retain their old role; parse the path filter or reject unsupported remove payloads instead of silently succeeding.
Useful? React with 👍 / 👎.
Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
…in-713 # Conflicts: # apps/web/tests/issue-detail-view.test.tsx
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 70ab16954d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| func scimBaseURL(r *http.Request, workspaceID string) string { | ||
| return strings.TrimRight(requestOrigin(r), "/") + "/api/scim/" + workspaceID | ||
| return strings.TrimRight(requestOrigin(r), "/") + "/scim/v2" |
There was a problem hiding this comment.
Route SCIM through the API prefix
In the documented ECS/ALB deployment I checked, scripts/preflight.sh routes only /api/* to the Go API and sends everything else to the web target, so this advertised /scim/v2 base URL is handled by the web service rather than the SCIM router. Enabling SCIM then gives IdPs a URL whose /Users and /Groups endpoints are unreachable outside direct API-port access; mount/advertise SCIM under /api/... or add the matching ALB rule.
Useful? React with 👍 / 👎.
| func resolveIssueID(ctx context.Context, db queryer, workspaceID, id string) (string, error) { | ||
| id = strings.TrimSpace(id) | ||
| var issueID string | ||
| err := db.QueryRow(ctx, `select i.id::text from issue i join team t on t.id=i.team_id where t.workspace_id=$1::uuid and (i.id::text=$2 or i.identifier=$2) limit 1`, workspaceID, id).Scan(&issueID) |
There was a problem hiding this comment.
Check private-team access before resolving issue links
When a workspace member who is not on a private team supplies that team's issue id or identifier to the customer-request link/create paths, this resolver still returns it because it only checks the workspace. The subsequent insert creates the customer-request association for a private issue, so a member can mutate private-team issue metadata by guessing an identifier; apply the same private-team predicate used elsewhere, allowing public teams, team members, or owner/admin roles.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aab56e1788
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| id := "agent-run-" + short(workspaceID) + "-" + itoa(len(runs)+1) | ||
| run := Run{ID: id, Title: input.Title, Prompt: input.Prompt, TeamKey: teamKey, Context: context, Status: "queued", Owner: owner, Target: teamKey + " · " + context, CreatedAt: now, UpdatedAt: now, Output: "Mock agent run queued. The next step is ready for review and can be promoted when a real executor is connected.", Logs: []string{"Created run from Agent dashboard composer.", "Captured context: " + teamKey + " · " + context + "."}, Suggestions: []Suggestion{suggestion(id+"-suggestion-open-issue", "Open linked workspace context", "Review the selected team and target context before handing this task to the real executor.", context, teamKey)}} | ||
| func (h Handler) listRuns(ctx context.Context, workspaceID string) ([]Run, error) { | ||
| rows, err := h.DB.Query(ctx, `select id::text,title,prompt,team_key,context,status,owner_name,target,created_at,updated_at,output,failure_reason,prompt_config,logs,suggestions from agent_run where workspace_id=$1::uuid order by updated_at desc, created_at desc`, workspaceID) |
There was a problem hiding this comment.
Filter agent-run history by private-team access
When an owner or private-team member creates an agent run for a private issue/team, any other workspace member who can open the agent dashboard receives it because this query only filters by workspace_id. The returned Run includes generated output/logs/suggestions, and workspaceSummary writes issue titles/descriptions and recent private issue titles into output, so private-team content remains visible even if create-time context resolution is tightened.
Useful? React with 👍 / 👎.
Co-authored-by: Hermes Controller <hermes-controller@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: df343e9ac3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| where itl.workspace_integration_id=$1::uuid and itl.provider='salesforce' and itl.source_event_id=$2 | ||
| limit 1`, integrationID, caseID).Scan(&issue.ID, &issue.Identifier, &issue.Title, &issue.TeamKey, &issue.StatusName, &issue.StatusCategory, &issue.Priority) |
There was a problem hiding this comment.
Filter private teams before returning Salesforce links
When a Salesforce case was linked while its issue's team was public and that team is later made private, SalesforceIssueCreate hits this existing-source lookup before creating anything and returns the issue id, identifier, title/status, and priority without the private-team predicate used by salesforceIssueForLink above. That lets Salesforce users with the signed component on that case view private issue metadata; apply the same coalesce(t.is_private,false)=false/archive filter here or treat it as not found.
Useful? React with 👍 / 👎.
| where itl.workspace_integration_id=$1::uuid and itl.provider='front' and itl.source_event_id=$2 | ||
| limit 1`, integrationID, frontCreatedSourceEventID(conversationID)).Scan(&issue.ID, &issue.Identifier, &issue.Title, &issue.TeamKey, &issue.StateName, &issue.Assignee) |
There was a problem hiding this comment.
Hide private Front-created issues on repeat create
When a Front-created issue is later moved under a private team, invoking the create action again for the same conversation returns this existing link before any public-team check, exposing the private issue's identifier, title, status, and assignee to the Front sidebar. frontIssueForLink already filters private and archived issues, so this existing-created lookup should apply the same predicate before returning the issue.
Useful? React with 👍 / 👎.
Controller-validated squash merge to staging for #588.
There was a problem hiding this comment.
💡 Codex Review
exponential/apps/api/internal/sync/handler.go
Line 139 in d082268
When a client reconnects after more than 1000 workspace operations were created while it was offline, this limit returns only the first page and the handler then switches to live pub/sub without any continuation or has_more signal. The SDK persists the last replayed version, so operations between that version and the current database max are skipped instead of being replayed; loop/paginate the backlog before entering the live stream.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| _ = wsjsonWrite(r.Context(), conn, replayMessage{Type: "replay", Operations: ops}) | ||
| recordReplayStatus(principal.WorkspaceID, lastVersion, len(ops)) | ||
|
|
||
| subscription, _ := SubscribeOperations(r.Context(), principal.WorkspaceID) |
There was a problem hiding this comment.
Subscribe before replaying sync operations
When an operation is committed after loadOperations finishes but before this SubscribeOperations call is active, it is neither included in the replay query nor delivered by Redis pub/sub. In that timing window a connected client misses the operation indefinitely, so issue/comment/project changes can leave other open workspaces stale until a reconnect or manual refresh; subscribe first and then replay from the requested version (or replay again after subscribing).
Useful? React with 👍 / 👎.
| ALTER TABLE customer ADD COLUMN IF NOT EXISTS source_external_id text; | ||
| ALTER TABLE customer ADD COLUMN IF NOT EXISTS metadata jsonb NOT NULL DEFAULT '{}'::jsonb; | ||
|
|
||
| ALTER TABLE customer_request ADD COLUMN IF NOT EXISTS issue_id uuid REFERENCES issue(id) ON DELETE SET NULL; |
There was a problem hiding this comment.
Backfill legacy customer-request issue links
For databases that already have Gong-created customer_request.issue_id values from the legacy schema, this reconcile migration preserves the legacy column but never inserts matching rows into issue_customer_request. The new customer request reads/counts hydrate links only from issue_customer_request, so upgraded feedback loses its related issue until each row is manually relinked; add an idempotent backfill from non-null customer_request.issue_id.
Useful? React with 👍 / 👎.
|
|
||
| func (h Handler) importJiraComments(ctx context.Context, tx pgx.Tx, p auth.Principal, install jiraInstall, members map[string]string, source jiraIssue, issueID string) (int, error) { | ||
| count := 0 | ||
| for _, comment := range source.Fields.Comment.Comments { |
There was a problem hiding this comment.
Fetch paginated Jira comments before importing
For Jira issues with more comments than the page embedded in the search result, this loop imports only source.Fields.Comment.Comments and never follows the issue comment pagination endpoint. Those older/additional comments are silently omitted while the import still reports success, so workspaces with long Jira discussions lose history during backfill/sync.
Useful? React with 👍 / 👎.
| i.created_at, i.updated_at, i.archived_at, i.completed_at, i.canceled_at | ||
| from issue i | ||
| join team t on t.id=i.team_id | ||
| where t.workspace_id=$1::uuid and ($2::timestamp is null or i.updated_at>$2::timestamp) |
There was a problem hiding this comment.
Use a lossless Airbyte cursor for tied timestamps
When more than limit records share the same updated_at value, the response sets next_cursor to the last returned timestamp and the next request filters with updated_at > cursor. Any remaining rows at that same timestamp are skipped; bulk imports/updates commonly create timestamp ties, so incremental Airbyte reads can miss records unless the cursor includes a tie-breaker such as (updated_at, id) or the query handles equality safely.
Useful? React with 👍 / 👎.
Summary
Promotes current
stagingtomain.Current delta
46192665fb7f43bfValidation
Pending rerun after latest staging reconciliation:
make check VERBOSE=1make test VERBOSE=1make test-e2e VERBOSE=1Notes
Release branch includes current staging plus release-only E2E stabilization commits from prior validation. Merge remains blocked until fresh local gates pass and GitHub branch policy allows merge.