From e86020d9d37a723c27cb78c1f1100bec8e0edd61 Mon Sep 17 00:00:00 2001 From: "madison.packer" Date: Thu, 2 Jul 2026 16:20:52 +0000 Subject: [PATCH 1/2] Add createClaimAttempt to Agents module Add POST /agents/claims/attempts endpoint support to the Node SDK. This allows admin API consumers to attach a user to a claim attempt and retrieve the code needed for the agent to complete the claim. Closes AUTH-6647 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/agents/agents.spec.ts | 72 +++++++++++++++++++ src/agents/agents.ts | 31 ++++++++ src/agents/fixtures/create-claim-attempt.json | 11 +++ .../interfaces/claim-attempt.interface.ts | 52 ++++++++++++++ src/agents/interfaces/index.ts | 1 + .../serializers/claim-attempt.serializer.ts | 32 +++++++++ src/agents/serializers/index.ts | 1 + 7 files changed, 200 insertions(+) create mode 100644 src/agents/fixtures/create-claim-attempt.json create mode 100644 src/agents/interfaces/claim-attempt.interface.ts create mode 100644 src/agents/serializers/claim-attempt.serializer.ts diff --git a/src/agents/agents.spec.ts b/src/agents/agents.spec.ts index 204f83433..fe31cdb37 100644 --- a/src/agents/agents.spec.ts +++ b/src/agents/agents.spec.ts @@ -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'; @@ -48,6 +49,77 @@ describe('Agents', () => { jest.mocked(jose.jwtVerify).mockReset(); }); + describe('createClaimAttempt', () => { + it('sends the request and deserializes the response', async () => { + fetchOnce(createClaimAttemptFixture); + + const result = await workos.agents.createClaimAttempt({ + claimAttemptToken: 'cla_tkn_01EHWNCE74X7JSDV0X3SZ3KJNY', + user: { + email: 'alice@example.com', + externalId: 'user_abc123', + }, + }); + + expect(fetchURL()).toContain('/agents/claims/attempts'); + expect(fetchBody()).toEqual({ + 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.createClaimAttempt({ + claimAttemptToken: 'cla_tkn_01EHWNCE74X7JSDV0X3SZ3KJNY', + user: { + email: 'alice@example.com', + externalId: 'user_abc123', + }, + organizationId: 'org_01EHZNVPK3SFK441A1RGBFSHRT', + }); + + expect(fetchBody()).toEqual({ + 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.createClaimAttempt({ + 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); diff --git a/src/agents/agents.ts b/src/agents/agents.ts index c2362612a..6cdc9feee 100644 --- a/src/agents/agents.ts +++ b/src/agents/agents.ts @@ -3,7 +3,10 @@ import { WorkOS } from '../workos'; import { AgentCredentialValidation, AgentRegistration, + ClaimAttemptResponse, + CreateClaimAttemptOptions, SerializedAgentAccessTokenClaims, + SerializedClaimAttemptResponse, SerializedAgentCredentialValidation, SerializedAgentRegistration, ValidateAgentAccessTokenOptions, @@ -13,6 +16,8 @@ import { deserializeAgentAccessTokenClaims, deserializeAgentCredentialValidation, deserializeAgentRegistration, + deserializeClaimAttemptResponse, + serializeCreateClaimAttemptOptions, serializeValidateAgentCredentialOptions, } from './serializers'; @@ -41,6 +46,32 @@ export class Agents { constructor(private readonly workos: WorkOS) {} + /** + * Confirm a claim attempt + * + * Attach a 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} + * @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 createClaimAttempt( + options: CreateClaimAttemptOptions, + ): Promise { + const { data } = await this.workos.post( + '/agents/claims/attempts', + serializeCreateClaimAttemptOptions(options), + ); + + return deserializeClaimAttemptResponse(data); + } + /** * Get an agent registration * diff --git a/src/agents/fixtures/create-claim-attempt.json b/src/agents/fixtures/create-claim-attempt.json new file mode 100644 index 000000000..101de6820 --- /dev/null +++ b/src/agents/fixtures/create-claim-attempt.json @@ -0,0 +1,11 @@ +{ + "id": "agent_reg_01EHZNVPK3SFK441A1RGBFSHRT", + "status": "unverified", + "user_code": "BCDF-GHJK", + "organizations": [ + { + "id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "name": "Acme Corp" + } + ] +} diff --git a/src/agents/interfaces/claim-attempt.interface.ts b/src/agents/interfaces/claim-attempt.interface.ts new file mode 100644 index 000000000..8a3c6a668 --- /dev/null +++ b/src/agents/interfaces/claim-attempt.interface.ts @@ -0,0 +1,52 @@ +import { AgentRegistrationStatus } from './agent-registration.interface'; + +/** Options for creating a claim attempt via the admin API. */ +export interface CreateClaimAttemptOptions { + /** 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 SerializedCreateClaimAttemptOptions { + 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 confirming 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[]; +} diff --git a/src/agents/interfaces/index.ts b/src/agents/interfaces/index.ts index 87ee656f1..eaa6bf490 100644 --- a/src/agents/interfaces/index.ts +++ b/src/agents/interfaces/index.ts @@ -1,2 +1,3 @@ export * from './agent-registration.interface'; +export * from './claim-attempt.interface'; export * from './validate-agent-credential.interface'; diff --git a/src/agents/serializers/claim-attempt.serializer.ts b/src/agents/serializers/claim-attempt.serializer.ts new file mode 100644 index 000000000..c0cc6172a --- /dev/null +++ b/src/agents/serializers/claim-attempt.serializer.ts @@ -0,0 +1,32 @@ +import { + ClaimAttemptResponse, + CreateClaimAttemptOptions, + SerializedClaimAttemptResponse, + SerializedCreateClaimAttemptOptions, +} from '../interfaces/claim-attempt.interface'; + +export function serializeCreateClaimAttemptOptions( + options: CreateClaimAttemptOptions, +): SerializedCreateClaimAttemptOptions { + return { + 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, + }; +} diff --git a/src/agents/serializers/index.ts b/src/agents/serializers/index.ts index 0c2a6f604..ccc8d7b73 100644 --- a/src/agents/serializers/index.ts +++ b/src/agents/serializers/index.ts @@ -1,2 +1,3 @@ export * from './agent-registration.serializer'; +export * from './claim-attempt.serializer'; export * from './validate-agent-credential.serializer'; From 2b0b86a28e2543500de5f42c2e88d1e74cb241b4 Mon Sep 17 00:00:00 2001 From: "madison.packer" Date: Thu, 2 Jul 2026 18:21:11 +0000 Subject: [PATCH 2/2] feat: Rename createClaimAttempt to linkClaimAttemptToExternalUser, add type discriminator Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/agents/agents.spec.ts | 10 ++++++---- src/agents/agents.ts | 18 +++++++++--------- .../interfaces/claim-attempt.interface.ts | 9 +++++---- .../serializers/claim-attempt.serializer.ts | 11 ++++++----- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/agents/agents.spec.ts b/src/agents/agents.spec.ts index fe31cdb37..3f727f61d 100644 --- a/src/agents/agents.spec.ts +++ b/src/agents/agents.spec.ts @@ -49,11 +49,11 @@ describe('Agents', () => { jest.mocked(jose.jwtVerify).mockReset(); }); - describe('createClaimAttempt', () => { + describe('linkClaimAttemptToExternalUser', () => { it('sends the request and deserializes the response', async () => { fetchOnce(createClaimAttemptFixture); - const result = await workos.agents.createClaimAttempt({ + const result = await workos.agents.linkClaimAttemptToExternalUser({ claimAttemptToken: 'cla_tkn_01EHWNCE74X7JSDV0X3SZ3KJNY', user: { email: 'alice@example.com', @@ -63,6 +63,7 @@ describe('Agents', () => { expect(fetchURL()).toContain('/agents/claims/attempts'); expect(fetchBody()).toEqual({ + type: 'link_external_user', claim_attempt_token: 'cla_tkn_01EHWNCE74X7JSDV0X3SZ3KJNY', user: { email: 'alice@example.com', @@ -85,7 +86,7 @@ describe('Agents', () => { it('includes organizationId when provided', async () => { fetchOnce(createClaimAttemptFixture); - await workos.agents.createClaimAttempt({ + await workos.agents.linkClaimAttemptToExternalUser({ claimAttemptToken: 'cla_tkn_01EHWNCE74X7JSDV0X3SZ3KJNY', user: { email: 'alice@example.com', @@ -95,6 +96,7 @@ describe('Agents', () => { }); expect(fetchBody()).toEqual({ + type: 'link_external_user', claim_attempt_token: 'cla_tkn_01EHWNCE74X7JSDV0X3SZ3KJNY', user: { email: 'alice@example.com', @@ -107,7 +109,7 @@ describe('Agents', () => { it('omits organizationId from the payload when not provided', async () => { fetchOnce(createClaimAttemptFixture); - await workos.agents.createClaimAttempt({ + await workos.agents.linkClaimAttemptToExternalUser({ claimAttemptToken: 'cla_tkn_01EHWNCE74X7JSDV0X3SZ3KJNY', user: { email: 'alice@example.com', diff --git a/src/agents/agents.ts b/src/agents/agents.ts index 6cdc9feee..8510591bc 100644 --- a/src/agents/agents.ts +++ b/src/agents/agents.ts @@ -4,7 +4,7 @@ import { AgentCredentialValidation, AgentRegistration, ClaimAttemptResponse, - CreateClaimAttemptOptions, + LinkClaimAttemptToExternalUserOptions, SerializedAgentAccessTokenClaims, SerializedClaimAttemptResponse, SerializedAgentCredentialValidation, @@ -17,7 +17,7 @@ import { deserializeAgentCredentialValidation, deserializeAgentRegistration, deserializeClaimAttemptResponse, - serializeCreateClaimAttemptOptions, + serializeLinkClaimAttemptToExternalUserOptions, serializeValidateAgentCredentialOptions, } from './serializers'; @@ -47,11 +47,11 @@ export class Agents { constructor(private readonly workos: WorkOS) {} /** - * Confirm a claim attempt + * Link a claim attempt to an external user * - * Attach a 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 + * 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. @@ -61,12 +61,12 @@ export class Agents { * @throws {ConflictException} 409 - Organization selection required, external ID conflict, or already claimed. * @throws {GoneException} 410 - Claim or user code expired. */ - async createClaimAttempt( - options: CreateClaimAttemptOptions, + async linkClaimAttemptToExternalUser( + options: LinkClaimAttemptToExternalUserOptions, ): Promise { const { data } = await this.workos.post( '/agents/claims/attempts', - serializeCreateClaimAttemptOptions(options), + serializeLinkClaimAttemptToExternalUserOptions(options), ); return deserializeClaimAttemptResponse(data); diff --git a/src/agents/interfaces/claim-attempt.interface.ts b/src/agents/interfaces/claim-attempt.interface.ts index 8a3c6a668..bbcf3eeb6 100644 --- a/src/agents/interfaces/claim-attempt.interface.ts +++ b/src/agents/interfaces/claim-attempt.interface.ts @@ -1,7 +1,7 @@ import { AgentRegistrationStatus } from './agent-registration.interface'; -/** Options for creating a claim attempt via the admin API. */ -export interface CreateClaimAttemptOptions { +/** 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. */ @@ -15,7 +15,8 @@ export interface CreateClaimAttemptOptions { organizationId?: string; } -export interface SerializedCreateClaimAttemptOptions { +export interface SerializedLinkClaimAttemptToExternalUserOptions { + type: 'link_external_user'; claim_attempt_token: string; user: { email: string; @@ -32,7 +33,7 @@ export interface ClaimAttemptOrganization { name: string; } -/** The result of confirming a claim attempt. */ +/** The result of linking an external user to a claim attempt. */ export interface ClaimAttemptResponse { /** The agent registration ID. */ id: string; diff --git a/src/agents/serializers/claim-attempt.serializer.ts b/src/agents/serializers/claim-attempt.serializer.ts index c0cc6172a..56d548306 100644 --- a/src/agents/serializers/claim-attempt.serializer.ts +++ b/src/agents/serializers/claim-attempt.serializer.ts @@ -1,14 +1,15 @@ import { ClaimAttemptResponse, - CreateClaimAttemptOptions, + LinkClaimAttemptToExternalUserOptions, SerializedClaimAttemptResponse, - SerializedCreateClaimAttemptOptions, + SerializedLinkClaimAttemptToExternalUserOptions, } from '../interfaces/claim-attempt.interface'; -export function serializeCreateClaimAttemptOptions( - options: CreateClaimAttemptOptions, -): SerializedCreateClaimAttemptOptions { +export function serializeLinkClaimAttemptToExternalUserOptions( + options: LinkClaimAttemptToExternalUserOptions, +): SerializedLinkClaimAttemptToExternalUserOptions { return { + type: 'link_external_user', claim_attempt_token: options.claimAttemptToken, user: { email: options.user.email,