Skip to content
Open
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
74 changes: 74 additions & 0 deletions src/agents/agents.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as jose from 'jose';
import fetch from 'jest-fetch-mock';
import { fetchBody, fetchOnce, fetchURL } from '../common/utils/test-utils';
import { WorkOS } from '../workos';
import createClaimAttemptFixture from './fixtures/create-claim-attempt.json';
import getAgentRegistrationFixture from './fixtures/get-agent-registration.json';
import validateAgentCredentialFixture from './fixtures/validate-agent-credential.json';

Expand Down Expand Up @@ -48,6 +49,79 @@ describe('Agents', () => {
jest.mocked(jose.jwtVerify).mockReset();
});

describe('linkClaimAttemptToExternalUser', () => {
it('sends the request and deserializes the response', async () => {
fetchOnce(createClaimAttemptFixture);

const result = await workos.agents.linkClaimAttemptToExternalUser({
claimAttemptToken: 'cla_tkn_01EHWNCE74X7JSDV0X3SZ3KJNY',
user: {
email: 'alice@example.com',
externalId: 'user_abc123',
},
});

expect(fetchURL()).toContain('/agents/claims/attempts');
expect(fetchBody()).toEqual({
type: 'link_external_user',
claim_attempt_token: 'cla_tkn_01EHWNCE74X7JSDV0X3SZ3KJNY',
user: {
email: 'alice@example.com',
external_id: 'user_abc123',
},
});
expect(result).toEqual({
id: 'agent_reg_01EHZNVPK3SFK441A1RGBFSHRT',
status: 'unverified',
userCode: 'BCDF-GHJK',
organizations: [
{
id: 'org_01EHZNVPK3SFK441A1RGBFSHRT',
name: 'Acme Corp',
},
],
});
});

it('includes organizationId when provided', async () => {
fetchOnce(createClaimAttemptFixture);

await workos.agents.linkClaimAttemptToExternalUser({
claimAttemptToken: 'cla_tkn_01EHWNCE74X7JSDV0X3SZ3KJNY',
user: {
email: 'alice@example.com',
externalId: 'user_abc123',
},
organizationId: 'org_01EHZNVPK3SFK441A1RGBFSHRT',
});

expect(fetchBody()).toEqual({
type: 'link_external_user',
claim_attempt_token: 'cla_tkn_01EHWNCE74X7JSDV0X3SZ3KJNY',
user: {
email: 'alice@example.com',
external_id: 'user_abc123',
},
organization_id: 'org_01EHZNVPK3SFK441A1RGBFSHRT',
});
});

it('omits organizationId from the payload when not provided', async () => {
fetchOnce(createClaimAttemptFixture);

await workos.agents.linkClaimAttemptToExternalUser({
claimAttemptToken: 'cla_tkn_01EHWNCE74X7JSDV0X3SZ3KJNY',
user: {
email: 'alice@example.com',
externalId: 'user_abc123',
},
});

const body = fetchBody();
expect(body).not.toHaveProperty('organization_id');
});
});

describe('getRegistration', () => {
it('retrieves an agent registration', async () => {
fetchOnce(getAgentRegistrationFixture);
Expand Down
31 changes: 31 additions & 0 deletions src/agents/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { WorkOS } from '../workos';
import {
AgentCredentialValidation,
AgentRegistration,
ClaimAttemptResponse,
LinkClaimAttemptToExternalUserOptions,
SerializedAgentAccessTokenClaims,
SerializedClaimAttemptResponse,
SerializedAgentCredentialValidation,
SerializedAgentRegistration,
ValidateAgentAccessTokenOptions,
Expand All @@ -13,6 +16,8 @@ import {
deserializeAgentAccessTokenClaims,
deserializeAgentCredentialValidation,
deserializeAgentRegistration,
deserializeClaimAttemptResponse,
serializeLinkClaimAttemptToExternalUserOptions,
serializeValidateAgentCredentialOptions,
} from './serializers';

Expand Down Expand Up @@ -41,6 +46,32 @@ export class Agents {

constructor(private readonly workos: WorkOS) {}

/**
* Link a claim attempt to an external user
*
* Link an external user to a claim attempt and retrieve the code needed
* for the agent to complete the claim. The user is looked up by external
* ID; if no user exists, one is created. When the user belongs to multiple
* organizations, an explicit organization must be provided.
*
* @param options - Object containing the claim attempt token, user details, and optional organization ID.
* @returns {Promise<ClaimAttemptResponse>}
* @throws {BadRequestException} 400 - Invalid request, email mismatch, or wrong account.
* @throws {ForbiddenException} 403 - Claim denied or auth method disabled.
* @throws {ConflictException} 409 - Organization selection required, external ID conflict, or already claimed.
* @throws {GoneException} 410 - Claim or user code expired.
*/
async linkClaimAttemptToExternalUser(
options: LinkClaimAttemptToExternalUserOptions,
): Promise<ClaimAttemptResponse> {
Comment on lines +64 to +66

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 Public API mismatch
The PR adds linkClaimAttemptToExternalUser, but the advertised SDK API and PR title/description are workos.agents.createClaimAttempt(...). With this implementation, consumers following the new documented example get workos.agents.createClaimAttempt is not a function, so the new endpoint wrapper is not exposed under the intended public method name.

Artifacts

Repro: focused runtime script that calls the advertised agents.createClaimAttempt API

  • Contains supporting evidence from the run (text/typescript; charset=utf-8).

Repro: command output showing createClaimAttempt is missing and linkClaimAttemptToExternalUser exists

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/agents.ts
Line: 64-66

Comment:
**Public API mismatch**
The PR adds `linkClaimAttemptToExternalUser`, but the advertised SDK API and PR title/description are `workos.agents.createClaimAttempt(...)`. With this implementation, consumers following the new documented example get `workos.agents.createClaimAttempt is not a function`, so the new endpoint wrapper is not exposed under the intended public method name.

How can I resolve this? If you propose a fix, please make it concise.

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.

This is resolved — the method was intentionally renamed from createClaimAttempt to linkClaimAttemptToExternalUser in the second commit. The PR title and description have been updated accordingly. The bot review was triggered against the first commit before the rename.

const { data } = await this.workos.post<SerializedClaimAttemptResponse>(
'/agents/claims/attempts',
serializeLinkClaimAttemptToExternalUserOptions(options),
);

return deserializeClaimAttemptResponse(data);
}

/**
* Get an agent registration
*
Expand Down
11 changes: 11 additions & 0 deletions src/agents/fixtures/create-claim-attempt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "agent_reg_01EHZNVPK3SFK441A1RGBFSHRT",
"status": "unverified",
"user_code": "BCDF-GHJK",
"organizations": [
{
"id": "org_01EHZNVPK3SFK441A1RGBFSHRT",
"name": "Acme Corp"
}
]
}
53 changes: 53 additions & 0 deletions src/agents/interfaces/claim-attempt.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { AgentRegistrationStatus } from './agent-registration.interface';

/** Options for linking an external user to a claim attempt via the admin API. */
export interface LinkClaimAttemptToExternalUserOptions {
/** The claim attempt token identifying the pending claim. */
claimAttemptToken: string;
/** The user to attach to the claim attempt. */
user: {
/** The email address of the user. */
email: string;
/** The external ID of the user. */
externalId: string;
};
/** The organization to place the agent in. Required when the user belongs to multiple organizations. */
organizationId?: string;
}

export interface SerializedLinkClaimAttemptToExternalUserOptions {
type: 'link_external_user';
claim_attempt_token: string;
user: {
email: string;
external_id: string;
};
organization_id?: string;
}

/** An organization the confirming user belongs to, offered as a placement choice. */
export interface ClaimAttemptOrganization {
/** The organization ID. */
id: string;
/** The organization name. */
name: string;
}

/** The result of linking an external user to a claim attempt. */
export interface ClaimAttemptResponse {
/** The agent registration ID. */
id: string;
/** Current status of the agent registration. */
status: AgentRegistrationStatus;
/** The user code the agent needs to complete the claim. */
userCode: string;
/** Organizations the user belongs to, offered as placement choices. */
organizations: ClaimAttemptOrganization[];
}

export interface SerializedClaimAttemptResponse {
id: string;
status: AgentRegistrationStatus;
user_code: string;
organizations: ClaimAttemptOrganization[];
}
1 change: 1 addition & 0 deletions src/agents/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './agent-registration.interface';
export * from './claim-attempt.interface';
export * from './validate-agent-credential.interface';
33 changes: 33 additions & 0 deletions src/agents/serializers/claim-attempt.serializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
ClaimAttemptResponse,
LinkClaimAttemptToExternalUserOptions,
SerializedClaimAttemptResponse,
SerializedLinkClaimAttemptToExternalUserOptions,
} from '../interfaces/claim-attempt.interface';

export function serializeLinkClaimAttemptToExternalUserOptions(
options: LinkClaimAttemptToExternalUserOptions,
): SerializedLinkClaimAttemptToExternalUserOptions {
return {
type: 'link_external_user',
claim_attempt_token: options.claimAttemptToken,
user: {
email: options.user.email,
external_id: options.user.externalId,
},
...(options.organizationId !== undefined && {
organization_id: options.organizationId,
}),
};
}

export function deserializeClaimAttemptResponse(
response: SerializedClaimAttemptResponse,
): ClaimAttemptResponse {
return {
id: response.id,
status: response.status,
userCode: response.user_code,
organizations: response.organizations,
};
}
1 change: 1 addition & 0 deletions src/agents/serializers/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './agent-registration.serializer';
export * from './claim-attempt.serializer';
export * from './validate-agent-credential.serializer';
Loading