Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-invites-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperdx/api': patch
---

Apply configured default connections and sources when a user accepts a team invite.
24 changes: 23 additions & 1 deletion packages/api/src/routers/api/__tests__/team.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import _ from 'lodash';
import { ObjectId } from 'mongodb';
import mongoose from 'mongoose';

import { getLoggedInAgent, getServer } from '@/fixtures';
import { getAgent, getLoggedInAgent, getServer } from '@/fixtures';
import Alert, { AlertSource, AlertThresholdType } from '@/models/alert';
import Team from '@/models/team';
import TeamInvite from '@/models/teamInvite';
import User from '@/models/user';
import * as setupDefaults from '@/setupDefaults';

describe('team router', () => {
const server = getServer();
Expand All @@ -16,6 +17,7 @@ describe('team router', () => {
});

afterEach(async () => {
jest.restoreAllMocks();
await server.clearDBs();
});

Expand Down Expand Up @@ -153,6 +155,26 @@ describe('team router', () => {
expect(resp.body.url).toContain(`/join-team?token=${teamInvite.token}`);
});

it('POST /team/setup/:token provisions defaults for the invited team', async () => {
const { team } = await getLoggedInAgent(server);
const invite = await TeamInvite.create({
email: 'invited@example.com',
name: 'Invited User',
teamId: team._id,
token: 'invite_token',
});
const setupTeamDefaults = jest
.spyOn(setupDefaults, 'setupTeamDefaults')
.mockResolvedValue();

await getAgent(server)
.post(`/team/setup/${invite.token}`)
.send({ password: 'TacoCat!2#4X' })
.expect(303);

expect(setupTeamDefaults).toHaveBeenCalledWith(team._id.toString());
});

it('POST /team/invitation with different case reuses existing invitation', async () => {
const { agent } = await getLoggedInAgent(server);

Expand Down
9 changes: 9 additions & 0 deletions packages/api/src/routers/api/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,15 @@ router.post('/team/setup/:token', async (req, res, next) => {
);
}

try {
await setupTeamDefaults(teamInvite.teamId.toString());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Provisioning resolves the wrong team

If the invited team is not the first team returned by MongoDB, this call reaches getTeam(teamId), which ignores the supplied ID, causing default connections and sources to be assigned to another tenant while the invited team remains unprovisioned.

Knowledge Base Used:

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

} catch (error) {
logger.error(
{ err: serializeError(error) },
'Failed to setup team defaults',
);
Comment on lines +171 to +177

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Provisioning lacks operational context

The added team-scoped provisioning call and its countable failure log attach neither business context nor a metric, preventing failures from being reliably attributed and counted by team during incident diagnosis.

Context Used: AGENTS.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

}

await TeamInvite.findByIdAndRemove(teamInvite._id);

req.login(user, err => {
Expand Down