Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
This_is_the_interface_for_interacting_with_the__Asana_Platform_httpsdevelopers_asana_com__Our_API_reference_is_generated_from_our__OpenAPI_spec__httpsraw_githubusercontent_comAsanaopenapimasterdefsasana_oas_yaml_
npm install asana --save
Include the latest release directly from GitHub:
<script src="https://github.com/Asana/node-asana/releases/download/v3.0.12/asana-min.js"></script>
Example usage (NOTE: be careful not to expose your access token):
<script>
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
let usersApiInstance = new Asana.UsersApi();
let user_gid = "me";
let opts = {};
usersApiInstance.getUser(user_gid, opts).then((result) => {
console.log('API called successfully. Returned data: ' + JSON.stringify(result.data, null, 2));
}, (error) => {
console.error(error.response.body);
});
</script>
Using Webpack you may encounter the following error: "Module not found: Error: Cannot resolve module", most certainly you should disable AMD loader. Add/merge the following section to your webpack config:
module: {
rules: [
{
parser: {
amd: false
}
}
]
}
Please follow the installation instruction and execute the following JS code:
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
let usersApiInstance = new Asana.UsersApi();
let user_gid = "me"; // String | A string identifying a user. This can either be the string \"me\", an email, or the gid of a user.
let opts = {
"opt_fields": "email,name,photo,photo.image_1024x1024,photo.image_128x128,photo.image_21x21,photo.image_27x27,photo.image_36x36,photo.image_60x60,workspaces,workspaces.name" // [String] | This endpoint returns a compact resource, which excludes some properties by default. To include those optional properties, set this query parameter to a comma-separated list of the properties you wish to include.
};
usersApiInstance.getUser(user_gid, opts).then((result) => {
console.log('API called successfully. Returned data: ' + JSON.stringify(result.data, null, 2));
}, (error) => {
console.error(error.response.body);
});
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
let tasksApiInstance = new Asana.TasksApi();
let opts = {
"limit": 50, // Number | Results per page. The number of objects to return per page. The value must be between 1 and 100.
"project": "<YOUR_PROJECT_GID>", // String | The project to filter tasks on.
"modified_since": "2012-02-22T02:06:58.158Z", // Date | Only return tasks that have been modified since the given time. *Note: A task is considered “modified” if any of its properties change, or associations between it and other objects are modified (e.g. a task being added to a project). A task is not considered modified just because another object it is associated with (e.g. a subtask) is modified. Actions that count as modifying the task include assigning, renaming, completing, and adding stories.*
"opt_fields": "actual_time_minutes,approval_status,assignee,assignee.name,assignee_section,assignee_section.name,assignee_status,completed,completed_at,completed_by,completed_by.name,created_at,created_by,custom_fields,custom_fields.asana_created_field,custom_fields.created_by,custom_fields.created_by.name,custom_fields.currency_code,custom_fields.custom_label,custom_fields.custom_label_position,custom_fields.date_value,custom_fields.date_value.date,custom_fields.date_value.date_time,custom_fields.description,custom_fields.display_value,custom_fields.enabled,custom_fields.enum_options,custom_fields.enum_options.color,custom_fields.enum_options.enabled,custom_fields.enum_options.name,custom_fields.enum_value,custom_fields.enum_value.color,custom_fields.enum_value.enabled,custom_fields.enum_value.name,custom_fields.format,custom_fields.has_notifications_enabled,custom_fields.is_formula_field,custom_fields.is_global_to_workspace,custom_fields.is_value_read_only,custom_fields.multi_enum_values,custom_fields.multi_enum_values.color,custom_fields.multi_enum_values.enabled,custom_fields.multi_enum_values.name,custom_fields.name,custom_fields.number_value,custom_fields.people_value,custom_fields.people_value.name,custom_fields.precision,custom_fields.resource_subtype,custom_fields.text_value,custom_fields.type,dependencies,dependents,due_at,due_on,external,external.data,followers,followers.name,hearted,hearts,hearts.user,hearts.user.name,html_notes,is_rendered_as_separator,liked,likes,likes.user,likes.user.name,memberships,memberships.project,memberships.project.name,memberships.section,memberships.section.name,modified_at,name,notes,num_hearts,num_likes,num_subtasks,offset,parent,parent.created_by,parent.name,parent.resource_subtype,path,permalink_url,projects,projects.name,resource_subtype,start_at,start_on,tags,tags.name,uri,workspace,workspace.name" // [String] | This endpoint returns a compact resource, which excludes some properties by default. To include those optional properties, set this query parameter to a comma-separated list of the properties you wish to include.
};
// GET - get multiple tasks
tasksApiInstance.getTasks(opts).then((result) => {
console.log('API called successfully. Returned data: ' + JSON.stringify(result.data, null, 2));
}, (error) => {
console.error(error.response.body);
});
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
let tasksApiInstance = new Asana.TasksApi();
let body = {
"data": {
"name": "New Task",
"approval_status": "pending",
"assignee_status": "upcoming",
"completed": false,
"external": {
"gid": "1234",
"data": "A blob of information.",
},
"html_notes": "<body>Mittens <em>really</em> likes the stuff from Humboldt.</body>",
"is_rendered_as_separator": false,
"liked": true,
"assignee": "me",
"projects": ["<YOUR_PROJECT_GID>"],
},
};
let opts = {};
// POST - Create a task
tasksApiInstance.createTask(body, opts).then((result) => {
console.log('API called successfully. Returned data: ' + JSON.stringify(result.data, null, 2));
}, (error) => {
console.error(error.response.body);
});
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
let tasksApiInstance = new Asana.TasksApi();
let task_gid = "<YOUR_TASK_GID>";
let body = {
"data": {
"name": "Updated Task",
},
};
let opts = {};
// PUT - Update a task
tasksApiInstance.updateTask(body, task_gid, opts).then((result) => {
console.log('API called successfully. Returned data: ' + JSON.stringify(result.data, null, 2));
}, (error) => {
console.error(error.response.body);
});
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
let tasksApiInstance = new Asana.TasksApi();
let task_gid = "<YOUR_TASK_GID>";
// DELETE - Delete a task
tasksApiInstance.deleteTask(task_gid).then((result) => {
console.log('API called successfully. Returned data: ' + JSON.stringify(result.data, null, 2));
}, (error) => {
console.error(error.response.body);
});
All URIs are relative to https://app.asana.com/api/1.0
Class | Method | HTTP request | Description |
---|---|---|---|
Asana.AllocationsApi | createAllocation | POST /allocations | Create an allocation |
Asana.AllocationsApi | deleteAllocation | DELETE /allocations/{allocation_gid} | Delete an allocation |
Asana.AllocationsApi | getAllocation | GET /allocations/{allocation_gid} | Get an allocation |
Asana.AllocationsApi | getAllocations | GET /allocations | Get multiple allocations |
Asana.AllocationsApi | updateAllocation | PUT /allocations/{allocation_gid} | Update an allocation |
Asana.AttachmentsApi | createAttachmentForObject | POST /attachments | Upload an attachment |
Asana.AttachmentsApi | deleteAttachment | DELETE /attachments/{attachment_gid} | Delete an attachment |
Asana.AttachmentsApi | getAttachment | GET /attachments/{attachment_gid} | Get an attachment |
Asana.AttachmentsApi | getAttachmentsForObject | GET /attachments | Get attachments from an object |
Asana.AuditLogAPIApi | getAuditLogEvents | GET /workspaces/{workspace_gid}/audit_log_events | Get audit log events |
Asana.BatchAPIApi | createBatchRequest | POST /batch | Submit parallel requests |
Asana.CustomFieldSettingsApi | getCustomFieldSettingsForPortfolio | GET /portfolios/{portfolio_gid}/custom_field_settings | Get a portfolio's custom fields |
Asana.CustomFieldSettingsApi | getCustomFieldSettingsForProject | GET /projects/{project_gid}/custom_field_settings | Get a project's custom fields |
Asana.CustomFieldsApi | createCustomField | POST /custom_fields | Create a custom field |
Asana.CustomFieldsApi | createEnumOptionForCustomField | POST /custom_fields/{custom_field_gid}/enum_options | Create an enum option |
Asana.CustomFieldsApi | deleteCustomField | DELETE /custom_fields/{custom_field_gid} | Delete a custom field |
Asana.CustomFieldsApi | getCustomField | GET /custom_fields/{custom_field_gid} | Get a custom field |
Asana.CustomFieldsApi | getCustomFieldsForWorkspace | GET /workspaces/{workspace_gid}/custom_fields | Get a workspace's custom fields |
Asana.CustomFieldsApi | insertEnumOptionForCustomField | POST /custom_fields/{custom_field_gid}/enum_options/insert | Reorder a custom field's enum |
Asana.CustomFieldsApi | updateCustomField | PUT /custom_fields/{custom_field_gid} | Update a custom field |
Asana.CustomFieldsApi | updateEnumOption | PUT /enum_options/{enum_option_gid} | Update an enum option |
Asana.EventsApi | getEvents | GET /events | Get events on a resource |
Asana.GoalRelationshipsApi | addSupportingRelationship | POST /goals/{goal_gid}/addSupportingRelationship | Add a supporting goal relationship |
Asana.GoalRelationshipsApi | getGoalRelationship | GET /goal_relationships/{goal_relationship_gid} | Get a goal relationship |
Asana.GoalRelationshipsApi | getGoalRelationships | GET /goal_relationships | Get goal relationships |
Asana.GoalRelationshipsApi | removeSupportingRelationship | POST /goals/{goal_gid}/removeSupportingRelationship | Removes a supporting goal relationship |
Asana.GoalRelationshipsApi | updateGoalRelationship | PUT /goal_relationships/{goal_relationship_gid} | Update a goal relationship |
Asana.GoalsApi | addFollowers | POST /goals/{goal_gid}/addFollowers | Add a collaborator to a goal |
Asana.GoalsApi | createGoal | POST /goals | Create a goal |
Asana.GoalsApi | createGoalMetric | POST /goals/{goal_gid}/setMetric | Create a goal metric |
Asana.GoalsApi | deleteGoal | DELETE /goals/{goal_gid} | Delete a goal |
Asana.GoalsApi | getGoal | GET /goals/{goal_gid} | Get a goal |
Asana.GoalsApi | getGoals | GET /goals | Get goals |
Asana.GoalsApi | getParentGoalsForGoal | GET /goals/{goal_gid}/parentGoals | Get parent goals from a goal |
Asana.GoalsApi | removeFollowers | POST /goals/{goal_gid}/removeFollowers | Remove a collaborator from a goal |
Asana.GoalsApi | updateGoal | PUT /goals/{goal_gid} | Update a goal |
Asana.GoalsApi | updateGoalMetric | POST /goals/{goal_gid}/setMetricCurrentValue | Update a goal metric |
Asana.JobsApi | getJob | GET /jobs/{job_gid} | Get a job by id |
Asana.MembershipsApi | createMembership | POST /memberships | Create a membership |
Asana.MembershipsApi | deleteMembership | DELETE /memberships/{membership_gid} | Delete a membership |
Asana.MembershipsApi | getMembership | GET /memberships/{membership_gid} | Get a membership |
Asana.MembershipsApi | getMemberships | GET /memberships | Get multiple memberships |
Asana.MembershipsApi | updateMembership | PUT /memberships/{membership_gid} | Update a membership |
Asana.OrganizationExportsApi | createOrganizationExport | POST /organization_exports | Create an organization export request |
Asana.OrganizationExportsApi | getOrganizationExport | GET /organization_exports/{organization_export_gid} | Get details on an org export request |
Asana.PortfolioMembershipsApi | getPortfolioMembership | GET /portfolio_memberships/{portfolio_membership_gid} | Get a portfolio membership |
Asana.PortfolioMembershipsApi | getPortfolioMemberships | GET /portfolio_memberships | Get multiple portfolio memberships |
Asana.PortfolioMembershipsApi | getPortfolioMembershipsForPortfolio | GET /portfolios/{portfolio_gid}/portfolio_memberships | Get memberships from a portfolio |
Asana.PortfoliosApi | addCustomFieldSettingForPortfolio | POST /portfolios/{portfolio_gid}/addCustomFieldSetting | Add a custom field to a portfolio |
Asana.PortfoliosApi | addItemForPortfolio | POST /portfolios/{portfolio_gid}/addItem | Add a portfolio item |
Asana.PortfoliosApi | addMembersForPortfolio | POST /portfolios/{portfolio_gid}/addMembers | Add users to a portfolio |
Asana.PortfoliosApi | createPortfolio | POST /portfolios | Create a portfolio |
Asana.PortfoliosApi | deletePortfolio | DELETE /portfolios/{portfolio_gid} | Delete a portfolio |
Asana.PortfoliosApi | getItemsForPortfolio | GET /portfolios/{portfolio_gid}/items | Get portfolio items |
Asana.PortfoliosApi | getPortfolio | GET /portfolios/{portfolio_gid} | Get a portfolio |
Asana.PortfoliosApi | getPortfolios | GET /portfolios | Get multiple portfolios |
Asana.PortfoliosApi | removeCustomFieldSettingForPortfolio | POST /portfolios/{portfolio_gid}/removeCustomFieldSetting | Remove a custom field from a portfolio |
Asana.PortfoliosApi | removeItemForPortfolio | POST /portfolios/{portfolio_gid}/removeItem | Remove a portfolio item |
Asana.PortfoliosApi | removeMembersForPortfolio | POST /portfolios/{portfolio_gid}/removeMembers | Remove users from a portfolio |
Asana.PortfoliosApi | updatePortfolio | PUT /portfolios/{portfolio_gid} | Update a portfolio |
Asana.ProjectBriefsApi | createProjectBrief | POST /projects/{project_gid}/project_briefs | Create a project brief |
Asana.ProjectBriefsApi | deleteProjectBrief | DELETE /project_briefs/{project_brief_gid} | Delete a project brief |
Asana.ProjectBriefsApi | getProjectBrief | GET /project_briefs/{project_brief_gid} | Get a project brief |
Asana.ProjectBriefsApi | updateProjectBrief | PUT /project_briefs/{project_brief_gid} | Update a project brief |
Asana.ProjectMembershipsApi | getProjectMembership | GET /project_memberships/{project_membership_gid} | Get a project membership |
Asana.ProjectMembershipsApi | getProjectMembershipsForProject | GET /projects/{project_gid}/project_memberships | Get memberships from a project |
Asana.ProjectStatusesApi | createProjectStatusForProject | POST /projects/{project_gid}/project_statuses | Create a project status |
Asana.ProjectStatusesApi | deleteProjectStatus | DELETE /project_statuses/{project_status_gid} | Delete a project status |
Asana.ProjectStatusesApi | getProjectStatus | GET /project_statuses/{project_status_gid} | Get a project status |
Asana.ProjectStatusesApi | getProjectStatusesForProject | GET /projects/{project_gid}/project_statuses | Get statuses from a project |
Asana.ProjectTemplatesApi | deleteProjectTemplate | DELETE /project_templates/{project_template_gid} | Delete a project template |
Asana.ProjectTemplatesApi | getProjectTemplate | GET /project_templates/{project_template_gid} | Get a project template |
Asana.ProjectTemplatesApi | getProjectTemplates | GET /project_templates | Get multiple project templates |
Asana.ProjectTemplatesApi | getProjectTemplatesForTeam | GET /teams/{team_gid}/project_templates | Get a team's project templates |
Asana.ProjectTemplatesApi | instantiateProject | POST /project_templates/{project_template_gid}/instantiateProject | Instantiate a project from a project template |
Asana.ProjectsApi | addCustomFieldSettingForProject | POST /projects/{project_gid}/addCustomFieldSetting | Add a custom field to a project |
Asana.ProjectsApi | addFollowersForProject | POST /projects/{project_gid}/addFollowers | Add followers to a project |
Asana.ProjectsApi | addMembersForProject | POST /projects/{project_gid}/addMembers | Add users to a project |
Asana.ProjectsApi | createProject | POST /projects | Create a project |
Asana.ProjectsApi | createProjectForTeam | POST /teams/{team_gid}/projects | Create a project in a team |
Asana.ProjectsApi | createProjectForWorkspace | POST /workspaces/{workspace_gid}/projects | Create a project in a workspace |
Asana.ProjectsApi | deleteProject | DELETE /projects/{project_gid} | Delete a project |
Asana.ProjectsApi | duplicateProject | POST /projects/{project_gid}/duplicate | Duplicate a project |
Asana.ProjectsApi | getProject | GET /projects/{project_gid} | Get a project |
Asana.ProjectsApi | getProjects | GET /projects | Get multiple projects |
Asana.ProjectsApi | getProjectsForTask | GET /tasks/{task_gid}/projects | Get projects a task is in |
Asana.ProjectsApi | getProjectsForTeam | GET /teams/{team_gid}/projects | Get a team's projects |
Asana.ProjectsApi | getProjectsForWorkspace | GET /workspaces/{workspace_gid}/projects | Get all projects in a workspace |
Asana.ProjectsApi | getTaskCountsForProject | GET /projects/{project_gid}/task_counts | Get task count of a project |
Asana.ProjectsApi | projectSaveAsTemplate | POST /projects/{project_gid}/saveAsTemplate | Create a project template from a project |
Asana.ProjectsApi | removeCustomFieldSettingForProject | POST /projects/{project_gid}/removeCustomFieldSetting | Remove a custom field from a project |
Asana.ProjectsApi | removeFollowersForProject | POST /projects/{project_gid}/removeFollowers | Remove followers from a project |
Asana.ProjectsApi | removeMembersForProject | POST /projects/{project_gid}/removeMembers | Remove users from a project |
Asana.ProjectsApi | updateProject | PUT /projects/{project_gid} | Update a project |
Asana.RulesApi | triggerRule | POST /rule_triggers/{rule_trigger_gid}/run | Trigger a rule |
Asana.SectionsApi | addTaskForSection | POST /sections/{section_gid}/addTask | Add task to section |
Asana.SectionsApi | createSectionForProject | POST /projects/{project_gid}/sections | Create a section in a project |
Asana.SectionsApi | deleteSection | DELETE /sections/{section_gid} | Delete a section |
Asana.SectionsApi | getSection | GET /sections/{section_gid} | Get a section |
Asana.SectionsApi | getSectionsForProject | GET /projects/{project_gid}/sections | Get sections in a project |
Asana.SectionsApi | insertSectionForProject | POST /projects/{project_gid}/sections/insert | Move or Insert sections |
Asana.SectionsApi | updateSection | PUT /sections/{section_gid} | Update a section |
Asana.StatusUpdatesApi | createStatusForObject | POST /status_updates | Create a status update |
Asana.StatusUpdatesApi | deleteStatus | DELETE /status_updates/{status_update_gid} | Delete a status update |
Asana.StatusUpdatesApi | getStatus | GET /status_updates/{status_update_gid} | Get a status update |
Asana.StatusUpdatesApi | getStatusesForObject | GET /status_updates | Get status updates from an object |
Asana.StoriesApi | createStoryForTask | POST /tasks/{task_gid}/stories | Create a story on a task |
Asana.StoriesApi | deleteStory | DELETE /stories/{story_gid} | Delete a story |
Asana.StoriesApi | getStoriesForTask | GET /tasks/{task_gid}/stories | Get stories from a task |
Asana.StoriesApi | getStory | GET /stories/{story_gid} | Get a story |
Asana.StoriesApi | updateStory | PUT /stories/{story_gid} | Update a story |
Asana.TagsApi | createTag | POST /tags | Create a tag |
Asana.TagsApi | createTagForWorkspace | POST /workspaces/{workspace_gid}/tags | Create a tag in a workspace |
Asana.TagsApi | deleteTag | DELETE /tags/{tag_gid} | Delete a tag |
Asana.TagsApi | getTag | GET /tags/{tag_gid} | Get a tag |
Asana.TagsApi | getTags | GET /tags | Get multiple tags |
Asana.TagsApi | getTagsForTask | GET /tasks/{task_gid}/tags | Get a task's tags |
Asana.TagsApi | getTagsForWorkspace | GET /workspaces/{workspace_gid}/tags | Get tags in a workspace |
Asana.TagsApi | updateTag | PUT /tags/{tag_gid} | Update a tag |
Asana.TaskTemplatesApi | deleteTaskTemplate | DELETE /task_templates/{task_template_gid} | Delete a task template |
Asana.TaskTemplatesApi | getTaskTemplate | GET /task_templates/{task_template_gid} | Get a task template |
Asana.TaskTemplatesApi | getTaskTemplates | GET /task_templates | Get multiple task templates |
Asana.TaskTemplatesApi | instantiateTask | POST /task_templates/{task_template_gid}/instantiateTask | Instantiate a task from a task template |
Asana.TasksApi | addDependenciesForTask | POST /tasks/{task_gid}/addDependencies | Set dependencies for a task |
Asana.TasksApi | addDependentsForTask | POST /tasks/{task_gid}/addDependents | Set dependents for a task |
Asana.TasksApi | addFollowersForTask | POST /tasks/{task_gid}/addFollowers | Add followers to a task |
Asana.TasksApi | addProjectForTask | POST /tasks/{task_gid}/addProject | Add a project to a task |
Asana.TasksApi | addTagForTask | POST /tasks/{task_gid}/addTag | Add a tag to a task |
Asana.TasksApi | createSubtaskForTask | POST /tasks/{task_gid}/subtasks | Create a subtask |
Asana.TasksApi | createTask | POST /tasks | Create a task |
Asana.TasksApi | deleteTask | DELETE /tasks/{task_gid} | Delete a task |
Asana.TasksApi | duplicateTask | POST /tasks/{task_gid}/duplicate | Duplicate a task |
Asana.TasksApi | getDependenciesForTask | GET /tasks/{task_gid}/dependencies | Get dependencies from a task |
Asana.TasksApi | getDependentsForTask | GET /tasks/{task_gid}/dependents | Get dependents from a task |
Asana.TasksApi | getSubtasksForTask | GET /tasks/{task_gid}/subtasks | Get subtasks from a task |
Asana.TasksApi | getTask | GET /tasks/{task_gid} | Get a task |
Asana.TasksApi | getTaskForCustomID | GET /workspaces/{workspace_gid}/tasks/custom_id/{custom_id} | Get a task for a given custom ID |
Asana.TasksApi | getTasks | GET /tasks | Get multiple tasks |
Asana.TasksApi | getTasksForProject | GET /projects/{project_gid}/tasks | Get tasks from a project |
Asana.TasksApi | getTasksForSection | GET /sections/{section_gid}/tasks | Get tasks from a section |
Asana.TasksApi | getTasksForTag | GET /tags/{tag_gid}/tasks | Get tasks from a tag |
Asana.TasksApi | getTasksForUserTaskList | GET /user_task_lists/{user_task_list_gid}/tasks | Get tasks from a user task list |
Asana.TasksApi | removeDependenciesForTask | POST /tasks/{task_gid}/removeDependencies | Unlink dependencies from a task |
Asana.TasksApi | removeDependentsForTask | POST /tasks/{task_gid}/removeDependents | Unlink dependents from a task |
Asana.TasksApi | removeFollowerForTask | POST /tasks/{task_gid}/removeFollowers | Remove followers from a task |
Asana.TasksApi | removeProjectForTask | POST /tasks/{task_gid}/removeProject | Remove a project from a task |
Asana.TasksApi | removeTagForTask | POST /tasks/{task_gid}/removeTag | Remove a tag from a task |
Asana.TasksApi | searchTasksForWorkspace | GET /workspaces/{workspace_gid}/tasks/search | Search tasks in a workspace |
Asana.TasksApi | setParentForTask | POST /tasks/{task_gid}/setParent | Set the parent of a task |
Asana.TasksApi | updateTask | PUT /tasks/{task_gid} | Update a task |
Asana.TeamMembershipsApi | getTeamMembership | GET /team_memberships/{team_membership_gid} | Get a team membership |
Asana.TeamMembershipsApi | getTeamMemberships | GET /team_memberships | Get team memberships |
Asana.TeamMembershipsApi | getTeamMembershipsForTeam | GET /teams/{team_gid}/team_memberships | Get memberships from a team |
Asana.TeamMembershipsApi | getTeamMembershipsForUser | GET /users/{user_gid}/team_memberships | Get memberships from a user |
Asana.TeamsApi | addUserForTeam | POST /teams/{team_gid}/addUser | Add a user to a team |
Asana.TeamsApi | createTeam | POST /teams | Create a team |
Asana.TeamsApi | getTeam | GET /teams/{team_gid} | Get a team |
Asana.TeamsApi | getTeamsForUser | GET /users/{user_gid}/teams | Get teams for a user |
Asana.TeamsApi | getTeamsForWorkspace | GET /workspaces/{workspace_gid}/teams | Get teams in a workspace |
Asana.TeamsApi | removeUserForTeam | POST /teams/{team_gid}/removeUser | Remove a user from a team |
Asana.TeamsApi | updateTeam | PUT /teams/{team_gid} | Update a team |
Asana.TimePeriodsApi | getTimePeriod | GET /time_periods/{time_period_gid} | Get a time period |
Asana.TimePeriodsApi | getTimePeriods | GET /time_periods | Get time periods |
Asana.TimeTrackingEntriesApi | createTimeTrackingEntry | POST /tasks/{task_gid}/time_tracking_entries | Create a time tracking entry |
Asana.TimeTrackingEntriesApi | deleteTimeTrackingEntry | DELETE /time_tracking_entries/{time_tracking_entry_gid} | Delete a time tracking entry |
Asana.TimeTrackingEntriesApi | getTimeTrackingEntriesForTask | GET /tasks/{task_gid}/time_tracking_entries | Get time tracking entries for a task |
Asana.TimeTrackingEntriesApi | getTimeTrackingEntry | GET /time_tracking_entries/{time_tracking_entry_gid} | Get a time tracking entry |
Asana.TimeTrackingEntriesApi | updateTimeTrackingEntry | PUT /time_tracking_entries/{time_tracking_entry_gid} | Update a time tracking entry |
Asana.TypeaheadApi | typeaheadForWorkspace | GET /workspaces/{workspace_gid}/typeahead | Get objects via typeahead |
Asana.UserTaskListsApi | getUserTaskList | GET /user_task_lists/{user_task_list_gid} | Get a user task list |
Asana.UserTaskListsApi | getUserTaskListForUser | GET /users/{user_gid}/user_task_list | Get a user's task list |
Asana.UsersApi | getFavoritesForUser | GET /users/{user_gid}/favorites | Get a user's favorites |
Asana.UsersApi | getUser | GET /users/{user_gid} | Get a user |
Asana.UsersApi | getUsers | GET /users | Get multiple users |
Asana.UsersApi | getUsersForTeam | GET /teams/{team_gid}/users | Get users in a team |
Asana.UsersApi | getUsersForWorkspace | GET /workspaces/{workspace_gid}/users | Get users in a workspace or organization |
Asana.WebhooksApi | createWebhook | POST /webhooks | Establish a webhook |
Asana.WebhooksApi | deleteWebhook | DELETE /webhooks/{webhook_gid} | Delete a webhook |
Asana.WebhooksApi | getWebhook | GET /webhooks/{webhook_gid} | Get a webhook |
Asana.WebhooksApi | getWebhooks | GET /webhooks | Get multiple webhooks |
Asana.WebhooksApi | updateWebhook | PUT /webhooks/{webhook_gid} | Update a webhook |
Asana.WorkspaceMembershipsApi | getWorkspaceMembership | GET /workspace_memberships/{workspace_membership_gid} | Get a workspace membership |
Asana.WorkspaceMembershipsApi | getWorkspaceMembershipsForUser | GET /users/{user_gid}/workspace_memberships | Get workspace memberships for a user |
Asana.WorkspaceMembershipsApi | getWorkspaceMembershipsForWorkspace | GET /workspaces/{workspace_gid}/workspace_memberships | Get the workspace memberships for a workspace |
Asana.WorkspacesApi | addUserForWorkspace | POST /workspaces/{workspace_gid}/addUser | Add a user to a workspace or organization |
Asana.WorkspacesApi | getWorkspace | GET /workspaces/{workspace_gid} | Get a workspace |
Asana.WorkspacesApi | getWorkspaces | GET /workspaces | Get multiple workspaces |
Asana.WorkspacesApi | removeUserForWorkspace | POST /workspaces/{workspace_gid}/removeUser | Remove a user from a workspace or organization |
Asana.WorkspacesApi | updateWorkspace | PUT /workspaces/{workspace_gid} | Update a workspace |
Our opt_fields
feature allows you to request for properties of a resource that you want to be returned in the response (more information here).
NOTE: by default, endpoints that return an array of results (EX: Get multiple tasks, Get multiple projects), will return a compact version of those results (EX: Get multiple tasks returns an array of TaskCompact objects).
opt_fields
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
let tasksApiInstance = new Asana.TasksApi();
let opts = {
"limit": 2,
"project": "<YOUR_PROJECT_GID>"
};
// GET - get multiple tasks
tasksApiInstance.getTasks(opts).then((result) => {
console.log(JSON.stringify(result.data, null, 2));
}, (error) => {
console.error(error.response.body);
});
[
{
"gid": "123",
"name": "Task 1",
"resource_type": "task",
"resource_subtype": "default_task"
},
{
"gid": "456",
"name": "Task 2",
"resource_type": "task",
"resource_subtype": "default_task"
}
]
opt_fields
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
let tasksApiInstance = new Asana.TasksApi();
let opts = {
"limit": 1,
"project": "<YOUR_PROJECT_GID>",
"opt_fields": "actual_time_minutes,approval_status,assignee,assignee.name,assignee_section,assignee_section.name,assignee_status,completed,completed_at,completed_by,completed_by.name,created_at,created_by,custom_fields,custom_fields.asana_created_field,custom_fields.created_by,custom_fields.created_by.name,custom_fields.currency_code,custom_fields.custom_label,custom_fields.custom_label_position,custom_fields.date_value,custom_fields.date_value.date,custom_fields.date_value.date_time,custom_fields.description,custom_fields.display_value,custom_fields.enabled,custom_fields.enum_options,custom_fields.enum_options.color,custom_fields.enum_options.enabled,custom_fields.enum_options.name,custom_fields.enum_value,custom_fields.enum_value.color,custom_fields.enum_value.enabled,custom_fields.enum_value.name,custom_fields.format,custom_fields.has_notifications_enabled,custom_fields.is_formula_field,custom_fields.is_global_to_workspace,custom_fields.is_value_read_only,custom_fields.multi_enum_values,custom_fields.multi_enum_values.color,custom_fields.multi_enum_values.enabled,custom_fields.multi_enum_values.name,custom_fields.name,custom_fields.number_value,custom_fields.people_value,custom_fields.people_value.name,custom_fields.precision,custom_fields.resource_subtype,custom_fields.text_value,custom_fields.type,dependencies,dependents,due_at,due_on,external,external.data,followers,followers.name,hearted,hearts,hearts.user,hearts.user.name,html_notes,is_rendered_as_separator,liked,likes,likes.user,likes.user.name,memberships,memberships.project,memberships.project.name,memberships.section,memberships.section.name,modified_at,name,notes,num_hearts,num_likes,num_subtasks,offset,parent,parent.created_by,parent.name,parent.resource_subtype,path,permalink_url,projects,projects.name,resource_subtype,start_at,start_on,tags,tags.name,uri,workspace,workspace.name"
};
// GET - get multiple tasks
tasksApiInstance.getTasks(opts).then((result) => {
console.log(JSON.stringify(result.data, null, 2));
}, (error) => {
console.error(error.response.body);
});
[
{
"gid": "129839839",
"actual_time_minutes": null,
"assignee": {
"gid": "120938293",
"name": "user@example.com"
},
"assignee_status": "upcoming",
"assignee_section": {
"gid": "1094838938",
"name": "Recently assigned"
},
"completed": false,
"completed_at": null,
"completed_by": null,
"created_at": "2023-01-01T20:31:21.717Z",
"created_by": {
"gid": "1201784467042440",
"resource_type": "user"
},
"custom_fields": [
{
"gid": "191859815",
"enabled": true,
"name": "Estimated time",
"description": "Asana-created. Estimate time to complete a task.",
"number_value": null,
"precision": 0,
"format": "duration",
"currency_code": null,
"custom_label": null,
"created_by": null,
"custom_label_position": null,
"display_value": null,
"resource_subtype": "number",
"is_formula_field": false,
"is_value_read_only": false,
"type": "number"
}
],
"dependencies": [],
"dependents": [],
"due_at": "2025-01-20T02:06:58.000Z",
"due_on": "2025-01-19",
"followers": [
{
"gid": "120938293",
"name": "user@example.com"
}
],
"hearted": true,
"hearts": [
{
"gid": "594849843",
"user": {
"gid": "120938293",
"name": "user@example.com"
}
}
],
"html_notes": "<body>Example task notes</body>",
"is_rendered_as_separator": false,
"liked": true,
"likes": [
{
"gid": "58303939",
"user": {
"gid": "120938293",
"name": "user@example.com"
}
}
],
"memberships": [
{
"project": {
"gid": "4567",
"name": "Example Project"
},
"section": {
"gid": "8900",
"name": "Untitled section"
}
}
],
"modified_at": "2023-01-25T21:24:06.996Z",
"name": "Task 1",
"notes": "Example task notes",
"num_hearts": 1,
"num_likes": 1,
"num_subtasks": 0,
"parent": null,
"permalink_url": "https://app.asana.com/0/58303939/129839839",
"projects": [
{
"gid": "4567",
"name": "Example Project"
}
],
"start_at": null,
"start_on": null,
"tags": [],
"resource_subtype": "default_task",
"workspace": {
"gid": "111111",
"name": "Example Workspace"
}
}
]
By default, endpoints that return an array of results (EX: Get multiple tasks, Get multiple projects), will return a Collection object.
This collection object contains a nextPage
method that can be used to fetch for the next page of results. NOTE: in order to use nextPage
you must have provided a limit
query parameter argument in the initial request.
You may run into the following error when making a request to an endpoint that has >1000 results:
"The result is too large. You should use pagination (may require specifying a workspace)!"
In this scenario you will want to use pagaintion to gather your results. To do this, you will need to provide a limit
query parameter argument in your request. This limit
query parameter represents the number of results per page. NOTE: the limit
can only be between 1 and 100.
EX: Pagination gather all resources
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
let tasksApiInstance = new Asana.TasksApi();
let opts = {
"project": "<YOUR_PROJECT_GID>",
"limit": 100,
};
async function getAllTasks(opts) {
let tasks = await tasksApiInstance.getTasks(opts).then(async (response) => {
let result = [];
let page = response;
while(true) {
// Add items on page to list of results
result = result.concat(page.data);
// Fetch the next page
page = await page.nextPage();
// If the there is no data in the next page break from the loop
if (!page.data) {
break;
}
}
return result;
}, (error) => {
console.error(error.response.body);
});
// Do something with the tasks. EX: print out results
console.log('Tasks: ' + JSON.stringify(tasks, null, 2));
}
getAllTasks(opts);
Sample output:
Tasks: [
{
"gid": "123",
"name": "task 1",
"resource_type": "task",
"resource_subtype": "default_task"
},
{
"gid": "456",
"name": "task 2",
"resource_type": "task",
"resource_subtype": "default_task"
},
{
"gid": "789",
"name": "task 3",
"resource_type": "task",
"resource_subtype": "default_task"
},
{
"gid": "101112",
"name": "task 4",
"resource_type": "task",
"resource_subtype": "default_task"
},
{
"gid": "131415",
"name": "task 5",
"resource_type": "task",
"resource_subtype": "default_task"
},
{
"gid": "161718",
"name": "task 6",
"resource_type": "task",
"resource_subtype": "default_task"
},
{
"gid": "192021",
"name": "task 7",
"resource_type": "task",
"resource_subtype": "default_task"
},
{
"gid": "222324",
"name": "task 8",
"resource_type": "task",
"resource_subtype": "default_task"
},
{
"gid": "252627",
"name": "task 9",
"resource_type": "task",
"resource_subtype": "default_task"
},
{
"gid": "282930",
"name": "task 10",
"resource_type": "task",
"resource_subtype": "default_task"
},
{
"gid": "313233",
"name": "task 11",
"resource_type": "task",
"resource_subtype": "default_task"
},
]
EX: Pagination do something per page
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
let tasksApiInstance = new Asana.TasksApi();
let opts = {
'project': "<YOUR_PROJECT_GID>",
"limit": 5,
};
let pageIndex = 1;
tasksApiInstance.getTasks(opts).then(async (response) => {
let page = response;
while(true) {
// Do something with the page results
// EX: print the name of the tasks on that page
console.log(`Page ${pageIndex}: `);
page.data.forEach(task => {
console.log(` ${task.name}`);
});
pageIndex += 1;
page = await page.nextPage();
// If the there is no data in the next page break from the loop
if (!page.data) {
break;
}
}
}, (error) => {
console.error(error.response.body);
});
Sample output:
Page 1:
task 1
task 2
task 3
task 4
task 5
Page 2:
task 6
task 7
task 8
task 9
task 10
Page 3:
task 11
task 12
task 13
task 14
task 15
If you do not want a Collection object returned and want to implement your own pagination, you can disable pagination by setting RETURN_COLLECTION
to false
:
EX: Turning off pagination
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
// Turn off pagination
client.RETURN_COLLECTION = false;
let tasksApiInstance = new Asana.TasksApi();
let opts = {
'project': "<YOUR_PROJECT_GID>",
'limit': 1
};
tasksApiInstance.getTasks(opts).then((result) => {
console.log('API called successfully. Returned data: ' + JSON.stringify(result, null, 2));
})
Sample response:
API called successfully. Returned data: {
"data": [
{
"gid": "<TASK_GID>",
"name": "Task 1",
"resource_type": "task",
"resource_subtype": "default_task"
},
],
"next_page": {
"offset": "gjJl2xAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJib3JkZXJfcmHilbI6IltcIlZ5IixcIjlaWlhVMkkzUUdOoXcEIsMTIwNDYxNTc0NTypNDI3MF0iLCJpYXQiOjE2OTc4MjgsSkjjQsImV4cCI6MTY5NzgyOTM2NH0.5VuMfKvqexoEsKfoPFtayWBNWiKvfR7_hN6MJaaIkx8",
"path": "/tasks?project=123456&limit=1&offset=gjJl2xAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJib3JkZXJfcmHilbI6IltcIlZ5IixcIjlaWlhVMkkzUUdOoXcEIsMTIwNDYxNTc0NTypNDI3MF0iLCJpYXQiOjE2OTc4MjgsSkjjQsImV4cCI6MTY5NzgyOTM2NH0.5VuMfKvqexoEsKfoPFtayWBNWiKvfR7_hN6MJaaIkx8",
"uri": "https://app.asana.com/api/1.0/tasks?project=123456&limit=1&offset=gjJl2xAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJib3JkZXJfcmHilbI6IltcIlZ5IixcIjlaWlhVMkkzUUdOoXcEIsMTIwNDYxNTc0NTypNDI3MF0iLCJpYXQiOjE2OTc4MjgsSkjjQsImV4cCI6MTY5NzgyOTM2NH0.5VuMfKvqexoEsKfoPFtayWBNWiKvfR7_hN6MJaaIkx8"
}
}
In order to get events you will need a sync token. This sync token can be acquired in the error message from the initial request to getEvents.
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
let eventsApiInstance = new Asana.EventsApi();
let resource = "<YOUR_TASK_OR_PROJECT_GID>"; // String | A resource ID to subscribe to. The resource can be a task or project.
let opts = {
"sync": ""
};
const timeouts = 5000
// Used to fetch for initial sync token
const setSyncToken = async () => {
await eventsApiInstance.getEvents(resource, opts).then((result) => {
console.log(JSON.stringify(result.data, null, 2));
}, (error) => {
let syncToken = error.response.body.sync;
opts['sync'] = syncToken;
});
}
const getEvents = async () => {
console.log("Setting sync token");
await setSyncToken();
// Fetch for new events every 5 seconds
console.log(`Fetching events every ${timeouts/1000} second(s)`);
while(true) {
await eventsApiInstance.getEvents(resource, opts).then((result) => {
// Print response
console.log(`Fetching events since sync: ${opts['sync']}`);
console.log(JSON.stringify(result.data, null, 2));
// Update the sync token with the new sync token provided in the response
opts['sync'] = result._response.sync;
}, (error) => {
if (error.status === 412) {
let syncToken = error.response.body.sync;
opts['sync'] = syncToken;
console.log(`412 error new sync token: ${syncToken}`);
} else{
console.error(error.response.text);
}
});
await new Promise(resolve => setTimeout(resolve, timeouts));
}
}
getEvents();
.
.
.
tasksApiInstance.getTask(task_gid, opts).then((task) => {
let taskName = task.data.name;
let taskNotes = task.data.notes;
console.log(`taskName: ${taskName}`);
console.log(`taskNotes: ${taskNotes}`);
}, (error) => {
console.error(error.response.body);
});
Use the <METHOD_NAME>WithHttpInfo
(EX: getTaskWithHttpInfo
) method to make a request that returns a response with headers.
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
let tasksApiInstance = new Asana.TasksApi();
let task_gid = "<YOUR_TASK_GID>";
let opts = {};
tasksApiInstance.getTaskWithHttpInfo(task_gid, opts).then((response_and_data) => {
let data = response_and_data.data;
let response = response_and_data.response;
let task = data.data;
let headers = response.headers;
console.log(task);
console.log(headers);
}, (error) => {
console.error(error.response.body);
});
EX: Asana-Enable header
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
// Add asana-enable header for the client
client.defaultHeaders['asana-enable'] = 'new_goal_memberships';
EX: Asana-Disable header
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
// Add asana-disable header for the client
client.defaultHeaders['asana-disable'] = 'new_goal_memberships';
callApi
methodUse the callApi
method to make http calls when the endpoint does not exist in the current library version or has bugs
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
// GET - get a task
client.callApi(
path='/tasks/{task_gid}',
httpMethod='GET',
pathParams={"task_gid": "<YOUR_TASK_GID>"},
queryParams={},
headerParams={},
formParams={},
bodyParam=null,
authNames=['token'],
contentTypes=[],
accepts=['application/json; charset=UTF-8'],
returnType='Blob'
).then((response_and_data) => {
let result = response_and_data.data;
let task = result.data;
console.log(task.name);
}, (error) => {
console.error(error.response.body);
});
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
// GET - get multiple tasks -> with opt_fields
client.callApi(
path='/tasks',
httpMethod='GET',
pathParams={},
queryParams={
"limit": 50,
"modified_since": '2012-02-22T02:06:58.158Z', // OR new Date("2012-02-22T02:06:58.158Z")
"project": '<YOUR_PROJECT_GID>',
"opt_fields": 'name,notes'
},
headerParams={},
formParams={},
bodyParam=null,
authNames=['token'],
contentTypes=[],
accepts=['application/json; charset=UTF-8'],
returnType='Blob'
).then((response_and_data) => {
let result = response_and_data.data;
let tasks = result.data;
if (tasks.length > 0) {
console.log(`Task 1 Name: ${tasks[0].name}`);
console.log(`Task 1 Notes: ${tasks[0].notes}`);
}
}, (error) => {
console.error(error.response.body);
});
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
// POST - create a task
client.callApi(
path='/tasks',
httpMethod='POST',
pathParams={},
queryParams={},
headerParams={},
formParams={},
bodyParam={
data: {
"name": "New Task",
"approval_status": "pending",
"assignee_status": "upcoming",
"completed": false,
"html_notes": "<body>Mittens <em>really</em> likes the stuff from Humboldt.</body>",
"is_rendered_as_separator": false,
"liked": true,
"assignee": "me",
"projects": ["<YOUR_PROJECT_GID>"],
}
},
authNames=['token'],
contentTypes=[],
accepts=['application/json; charset=UTF-8'],
returnType='Blob'
).then((response_and_data) => {
let result = response_and_data.data;
let task = result.data;
console.log(task.name);
}, (error) => {
console.error(error.response.body);
});
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
// PUT - update a task
client.callApi(
path='/tasks/{task_gid}',
httpMethod='PUT',
pathParams={task_gid: "<YOUR_TASK_GID>"},
queryParams={},
headerParams={},
formParams={},
bodyParam={
"data": {
"name": "Updated Task",
"html_notes": "<body>Updated task notes</body>",
"due_at": "2025-01-20T02:06:58.147Z"
}
},
authNames=['token'],
contentTypes=[],
accepts=['application/json; charset=UTF-8'],
returnType='Blob'
).then((response_and_data) => {
let result = response_and_data.data;
let task = result.data;
console.log(task.name);
}, (error) => {
console.error(error.response.body);
});
const Asana = require('asana');
let client = Asana.ApiClient.instance;
let token = client.authentications['token'];
token.accessToken = '<YOUR_ACCESS_TOKEN>';
// DELETE - delete a task
client.callApi(
path='/tasks/{task_gid}',
httpMethod='DELETE',
pathParams={"task_gid": "<YOUR_TASK_GID>"},
queryParams={},
headerParams={},
formParams={},
bodyParam=null,
authNames=['token'],
contentTypes=[],
accepts=['application/json; charset=UTF-8'],
returnType='Blob'
).then((response_and_data) => {
let result = response_and_data.data;
let result = result.data;
console.log(result);
}, (error) => {
console.error(error.response.body);
});
FAQs
This_is_the_interface_for_interacting_with_the__Asana_Platform_httpsdevelopers_asana_com__Our_API_reference_is_generated_from_our__OpenAPI_spec__httpsraw_githubusercontent_comAsanaopenapimasterdefsasana_oas_yaml_
The npm package asana receives a total of 44,593 weekly downloads. As such, asana popularity was classified as popular.
We found that asana demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.