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 thread
m0tzy marked this conversation as resolved.
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