@taskp3/sdk
v0.1.1
Published
TaskP3 SDK — server-side client and Express adapter
Readme
@taskp3/sdk
Server-side SDK for TaskP3 — API client and Express adapter.
For React components, see @taskp3/react.
Installation
npm install @taskp3/sdkWhy Use the SDK?
Without the SDK, integrating TaskP3 means writing and maintaining a lot of plumbing yourself. Here's what our own app looked like before adopting the SDK:
~200 lines of API service functions:
// services/triage.ts — hand-rolled fetch wrappers for every endpoint
export const createTriage = async ({
name,
description,
path,
type,
loomUrl,
fileIds,
}) => {
const res = await api.post("/triage/create", {
name,
description,
path,
type,
loomUrl,
fileIds,
});
return res.data;
};
export const uploadTriageFile = async ({ data }) => {
const res = await api.post("/triage/file", data);
return res.data;
};
export const getTriageSubmissions = async ({
sortOrder,
open,
type,
page,
limit,
}) => {
/* ... */
};
export const getTriageFile = async ({ fileId }) => {
/* ... */
};
export const getTriageSubmissionResponses = async ({ taskId, page, limit }) => {
/* ... */
};
export const getChangelog = async ({ page, limit, types }) => {
/* ... */
};
// ... plus all the TypeScript interfaces~175 lines of React Query hooks:
// hooks/triage.tsx — pagination, caching, stale times, etc.
export function useUserTriageSubmissions({
externalCreatedById,
sortOrder,
open,
type,
limit,
}) {
return useInfiniteQuery(
[
"user",
externalCreatedById,
"triage_submissions",
sortOrder,
open,
type,
limit,
],
({ pageParam = 1 }) =>
getTriageSubmissions({ sortOrder, open, type, page: pageParam, limit }),
{
getNextPageParam: (lastPage) =>
lastPage.page < lastPage.totalPages ? lastPage.page + 1 : undefined,
},
);
}
export function useChangelog({ limit, types }) {
/* ... */
}
export function useLatestChangelog() {
/* ... */
}
export function useChangelogCodeChanges({ changelogId }) {
/* ... */
}~500 lines of triage UI (category tabs, file upload, presigned URL handling, paste support):
// components/triage/triageMenu.tsx — reimplementing the widget from scratch
const handleFileUpload = async (files: FileList) => {
const filePromises = Array.from(files).map(async (file) => {
const { file: fileData, signedUrl } = await uploadTriageFile({
data: {
fileName: file.name,
contentType: file.type,
fileSize: file.size,
},
});
await axios.put(signedUrl, file, {
headers: { "Content-Type": file.type },
});
return {
id: fileData.id,
name: file.name,
size: file.size,
type: file.type,
};
});
// ...error handling, state updates, alerts...
};~100 lines of Zustand state management, ~80 lines of changelog modal auto-show logic, and more.
With the SDK, all of that becomes:
// Server — 1 route mount replaces your custom API layer
app.use(
"/api/triage",
createTriageRouter({
apiKey: process.env.TASKP3_API_KEY!,
projectId: process.env.TASKP3_PROJECT_ID!,
authMiddleware: requireAuth,
resolveUser: (req) => ({ id: req.user.id, name: req.user.name }),
}),
);// Client — shared components replace a large amount of UI, hooks, and services
<TaskP3Provider
apiUrl="https://your-app.com/api/triage"
getHeaders={() => ({ Authorization: `Bearer ${token}` })}
>
<App />
<TriageButton />
<WhatsNew />
</TaskP3Provider>The SDK handles presigned uploads, pagination, caching, auto-show logic, theming, and many of the integration edge cases so you don't have to.
For CP9-style parity, the strongest match today is the initial submission happy path when @taskp3/react is used with the Express adapter. Past-submissions, reply composition, host routing, and alert orchestration still have documented parity gaps. See CP_PARITY_MATRIX.md for the current status.
Usage
API Client
import TaskP3 from "@taskp3/sdk";
const taskp3 = new TaskP3("sk_live_...", { projectId: "proj_..." });
const submission = await taskp3.triage.createTask({
taskName: "Safari button broken",
description: "The submit button doesn't respond on Safari 17",
type: "Problem",
externalCreatedById: "user_123",
externalCreatedByName: "Jane Doe",
});Express Adapter
Mount the router to proxy frontend requests — keeps your API key server-side:
Supports express 4.18+ and 5.x.
import express from "express";
import { createTriageRouter, TRIAGE_ROUTE_PATHS } from "@taskp3/sdk";
const app = express();
app.use(
"/api/triage",
createTriageRouter({
apiKey: process.env.TASKP3_API_KEY!,
projectId: process.env.TASKP3_PROJECT_ID!,
// Plug in your existing auth middleware
authMiddleware: requireAuth,
// Optional per-route middleware using SDK route constants
routeMiddleware: {
[TRIAGE_ROUTE_PATHS.CREATE_SUBMISSION]: requireCanCreateTriage,
[TRIAGE_ROUTE_PATHS.LIST_SUBMISSIONS]: requireCanReadTriage,
},
// Map your user to a TaskP3 user (id + name are required)
resolveUser: (req) => ({
id: req.user.id,
name: req.user.name,
}),
// Attach arbitrary metadata to every submission
resolveMetaData: (req) => ({
orgId: req.user.orgId,
plan: req.user.plan,
}),
}),
);The router includes express.json() internally — no need to apply it yourself.
API Reference
new TaskP3(apiKey, options)
| Param | Type | Required | Description |
| ------------------- | -------- | -------- | --------------------------------------------------------- |
| apiKey | string | Yes | Your TaskP3 secret key |
| options.projectId | string | Yes | Project identifier |
| options.apiUrl | string | No | Override API base URL (default: https://api.taskp3.com) |
Returns a TaskP3 instance with the following resource:
taskp3.triage
| Method | Returns | Description |
| ----------------------------------------------- | --------------------------------------- | ---------------------------------- |
| createTask(data) | Promise<TriageSubmission> | Create a triage submission |
| getSubmission(taskId) | Promise<TriageSubmission> | Get a single submission |
| getSubmissions(params) | Promise<TriageSubmissionsResponse> | List submissions (paginated) |
| getSubmissionResponses(taskId, params?) | Promise<TriageResponsesResult> | List responses for a submission |
| createResponse(taskId, data) | Promise<TriageSubmission> | Add a response to a submission |
| uploadFile(data) | Promise<TriageFileUpload> | Get a presigned upload URL |
| createSubmissionFile(taskId, data) | Promise<TriageLinkedFileUploadResponse> | Create and attach submission file |
| createResponseFile(responseId, data) | Promise<TriageLinkedFileUploadResponse> | Create and attach response file |
| getFile(fileId) | Promise<TriageFile> | Get file metadata + download URL |
| getSubmissionFiles(taskId, params?) | Promise<TriageFilesResponse> | List files on a submission |
| getResponseFiles(responseId, params?) | Promise<TriageFilesResponse> | List files on a response |
| getChangelog(params?) | Promise<ChangelogResponse> | List changelog entries (paginated) |
| getChangelogCodeChanges(changelogId, params?) | Promise<ChangelogCodeChangesResponse> | List code changes for an entry |
createTriageRouter(config)
| Param | Type | Required | Description |
| ----------------- | ---------------------------------- | -------- | --------------------------------------------------------- |
| apiKey | string | Yes | Your TaskP3 secret key |
| projectId | string | Yes | Project identifier |
| apiUrl | string | No | Optional API URL override (default: https://api.taskp3.com) |
| resolveUser | (req) => TriageUser | Yes | Map your request to a TaskP3 user ({ id, name, type? }) |
| authMiddleware | RequestHandler | No | Guard that runs before every route |
| routeMiddleware | Partial<Record<TriageRoutePath, RequestHandler \| RequestHandler[]>> | No | Per-route middleware (runs after authMiddleware when both are set; prefer TRIAGE_ROUTE_PATHS.* keys) |
| resolveMetaData | (req) => Record<string, unknown> | No | Extra metadata attached to submissions |
| resolveExternalCreatedById | ({ req, user, submitter }) => string \| string[] \| undefined | No | Override submission user scope (for me/team/all flows) |
| resolveUserScope | ({ req, user, submitter }) => string[] | No | Resolve scoped user ids from role/tenant context |
| buildTaskName | ({ req, type, description, user }) => string | No | Custom task naming strategy |
| resolveTags | (req) => TriageTag[] | No | Attach tags on create |
For hierarchical scoping, you can use createHierarchicalScopeResolver() from @taskp3/sdk.
taskp3.webhooks
Webhook endpoint management client:
| Method | Returns | Description |
|---|---|---|
| createEndpoint(projectId, data) | Promise<CreateWebhookEndpointResponse> | Create endpoint and return signing secret |
| getEndpoints(projectId) | Promise<WebhookEndpoint[]> | List project endpoints |
| getEndpoint(projectId, endpointId) | Promise<WebhookEndpoint> | Get one endpoint |
| updateEndpoint(projectId, endpointId, data) | Promise<WebhookEndpoint> | Update endpoint |
| deleteEndpoint(projectId, endpointId) | Promise<{ message: string }> | Delete endpoint |
| rollSecret(projectId, endpointId) | Promise<RollWebhookSecretResponse> | Rotate endpoint secret |
| getEvents(projectId, params?) | Promise<WebhookEventRecord[]> | List recent webhook events |
Webhook Verification Helpers
Use SDK helpers in your webhook receiver:
import {
getRawRequestBody,
verifyP3WebhookSignature,
parseP3WebhookEvent,
} from "@taskp3/sdk";Helpers:
getRawRequestBody(body)converts raw express body to UTF-8 stringverifyP3WebhookSignature({ rawBody, signatureHeader, secret })validatesx-p3-signatureparseP3WebhookEvent(rawBody)validates and parses event payload
Parity and Migration
- Internal parity mapping:
CP_PARITY_MATRIX.md - Migration recipes from custom integration services/routes are documented in code examples above.
- Transcript APIs were removed from
@taskp3/sdkin this SDK pass (getFileTranscriptand the Express/file/:fileId/transcriptadapter route).
Related Packages
| Package | Description |
| -------------------------------------------------------------- | ---------------------------------------------------------------- |
| @taskp3/sdk | Server-side client + Express adapter |
| @taskp3/react | React components — <TriageButton>, <Changelog>, <WhatsNew> |
