@loculabs/api-client 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/webhook.ts"],"sourcesContent":["// Client\nexport { createLocuClient, LocuApiError } from \"./client\"\nexport type { LocuClientConfig, LocuClient } from \"./client\"\n\n// Webhook utilities\nexport {\n verifyWebhookSignature,\n parseWebhookSignature,\n parseWebhookPayload,\n generateWebhookSignature,\n} from \"./webhook\"\nexport type {\n WebhookSignatureResult,\n ParsedWebhookSignature,\n VerifyWebhookOptions,\n} from \"./webhook\"\n\nexport type * from \"./generated/api\"\n","import type {\n ActivityListParams,\n ApiError,\n CreateActivityRequest,\n CreateNoteRequest,\n CreateProjectRequest,\n CreateSessionRequest,\n CreateTaskRequest,\n CreateWebhookRequest,\n MeResponse,\n Note,\n NoteListParams,\n PaginatedResponse,\n PaginationParams,\n Project,\n ProjectListParams,\n Session,\n SessionActivity,\n SessionListParams,\n SessionWithActivities,\n StartTimerRequest,\n StopTimerResponse,\n SubtaskListParams,\n Task,\n TaskListParams,\n TaskSectionsParams,\n TaskSectionsResponse,\n TimerState,\n UpdateActivityRequest,\n UpdateNoteRequest,\n UpdateProjectRequest,\n UpdateSessionRequest,\n UpdateTaskRequest,\n UpdateWebhookRequest,\n Webhook,\n WebhookDelivery,\n WebhookListParams,\n WebhookWithSecret,\n} from \"./types\"\n\nexport type LocuClientConfig = {\n /** API base URL (defaults to https://api.locu.app/api/v1) */\n baseUrl?: string\n /** Personal Access Token for authentication */\n token: string\n /** Custom fetch implementation (defaults to global fetch) */\n fetch?: typeof fetch\n}\n\nexport class LocuApiError extends Error {\n status: number\n code?: string\n\n constructor(message: string, status: number, code?: string) {\n super(message)\n this.name = \"LocuApiError\"\n this.status = status\n this.code = code\n }\n}\n\nconst buildQueryString = (params: Record<string, unknown>): string => {\n const searchParams = new URLSearchParams()\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== null) {\n searchParams.set(key, String(value))\n }\n }\n const qs = searchParams.toString()\n return qs ? `?${qs}` : \"\"\n}\n\nexport const createLocuClient = (config: LocuClientConfig) => {\n const baseUrl = config.baseUrl || \"https://api.locu.app/api/v1\"\n const fetchFn = config.fetch || fetch\n\n const request = async <T>(\n method: string,\n path: string,\n body?: unknown\n ): Promise<T> => {\n const url = `${baseUrl}${path}`\n const headers: Record<string, string> = {\n Authorization: `Bearer ${config.token}`,\n \"Content-Type\": \"application/json\",\n }\n\n const response = await fetchFn(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n })\n\n if (!response.ok) {\n let errorData: ApiError | null = null\n try {\n errorData = await response.json()\n } catch {\n // Ignore JSON parse errors\n }\n throw new LocuApiError(\n errorData?.message || `Request failed with status ${response.status}`,\n response.status,\n errorData?.code\n )\n }\n\n // Handle 204 No Content\n if (response.status === 204) {\n return undefined as T\n }\n\n return response.json()\n }\n\n return {\n // ============ Me ============\n me: {\n /** Get current me */\n get: (): Promise<MeResponse> => request(\"GET\", \"/me\"),\n },\n\n // ============ Timer ============\n timer: {\n /** Get current timer */\n get: (): Promise<TimerState> => request(\"GET\", \"/timer\"),\n\n /** Start a new timer */\n start: (data?: StartTimerRequest): Promise<TimerState> =>\n request(\"POST\", \"/timer/start\", data),\n\n /** Pause the running timer */\n pause: (): Promise<TimerState> => request(\"POST\", \"/timer/pause\"),\n\n /** Resume a paused timer */\n continue: (): Promise<TimerState> => request(\"POST\", \"/timer/continue\"),\n\n /** Stop timer and save sessions */\n stop: (): Promise<StopTimerResponse> => request(\"POST\", \"/timer/stop\"),\n\n /** Cancel timer without saving sessions */\n cancel: (): Promise<TimerState> => request(\"DELETE\", \"/timer\"),\n },\n\n // ============ Tasks ============\n tasks: {\n /** List all tasks */\n list: (params: TaskListParams = {}): Promise<PaginatedResponse<Task>> =>\n request(\"GET\", `/tasks${buildQueryString(params)}`),\n\n /** Get a single task by ID */\n get: (id: string): Promise<Task> => request(\"GET\", `/tasks/${id}`),\n\n /** Create a new task */\n create: (data: CreateTaskRequest): Promise<Task> =>\n request(\"POST\", \"/tasks\", data),\n\n /** Update an existing task */\n update: (id: string, data: UpdateTaskRequest): Promise<Task> =>\n request(\"PATCH\", `/tasks/${id}`, data),\n\n /** Delete a task */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/tasks/${id}`),\n\n /** Get tasks organized by section (today, sooner, later) */\n sections: (\n params: TaskSectionsParams = {}\n ): Promise<TaskSectionsResponse> =>\n request(\"GET\", `/tasks/sections${buildQueryString(params)}`),\n\n /** List subtasks for a task */\n subtasks: (\n id: string,\n params: SubtaskListParams = {}\n ): Promise<PaginatedResponse<Task>> =>\n request(\"GET\", `/tasks/${id}/subtasks${buildQueryString(params)}`),\n\n /** Create a subtask under a parent task */\n createSubtask: (\n parentId: string,\n data: Omit<CreateTaskRequest, \"parentId\" | \"section\">\n ): Promise<Task> => request(\"POST\", \"/tasks\", { ...data, parentId }),\n },\n\n // ============ Projects ============\n projects: {\n /** List all projects */\n list: (\n params: ProjectListParams = {}\n ): Promise<PaginatedResponse<Project>> =>\n request(\"GET\", `/projects${buildQueryString(params)}`),\n\n /** Get a single project by ID */\n get: (id: string): Promise<Project> => request(\"GET\", `/projects/${id}`),\n\n /** Create a new project */\n create: (data: CreateProjectRequest): Promise<Project> =>\n request(\"POST\", \"/projects\", data),\n\n /** Update an existing project */\n update: (id: string, data: UpdateProjectRequest): Promise<Project> =>\n request(\"PATCH\", `/projects/${id}`, data),\n\n /** Delete a project */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/projects/${id}`),\n },\n\n // ============ Notes ============\n notes: {\n /** List all notes */\n list: (params: NoteListParams = {}): Promise<PaginatedResponse<Note>> =>\n request(\"GET\", `/notes${buildQueryString(params)}`),\n\n /** Get a single note by ID */\n get: (id: string): Promise<Note> => request(\"GET\", `/notes/${id}`),\n\n /** Create a new note */\n create: (data: CreateNoteRequest): Promise<Note> =>\n request(\"POST\", \"/notes\", data),\n\n /** Update an existing note */\n update: (id: string, data: UpdateNoteRequest): Promise<Note> =>\n request(\"PATCH\", `/notes/${id}`, data),\n\n /** Delete a note */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/notes/${id}`),\n },\n\n // ============ Sessions ============\n sessions: {\n /** List all sessions */\n list: (\n params: SessionListParams = {}\n ): Promise<PaginatedResponse<SessionWithActivities>> =>\n request(\"GET\", `/sessions${buildQueryString(params)}`),\n\n /** Get a single session by ID */\n get: (id: string): Promise<SessionWithActivities> =>\n request(\"GET\", `/sessions/${id}`),\n\n /** Create a new session */\n create: (data: CreateSessionRequest): Promise<Session> =>\n request(\"POST\", \"/sessions\", data),\n\n /** Update an existing session */\n update: (id: string, data: UpdateSessionRequest): Promise<Session> =>\n request(\"PATCH\", `/sessions/${id}`, data),\n\n /** Delete a session */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/sessions/${id}`),\n\n /** List all activities with optional filters */\n listActivities: (\n params: ActivityListParams = {}\n ): Promise<PaginatedResponse<SessionActivity>> =>\n request(\"GET\", `/sessions/activities${buildQueryString(params)}`),\n\n // Activities\n activities: {\n /** List activities for a session */\n list: (sessionId: string): Promise<{ data: SessionActivity[] }> =>\n request(\"GET\", `/sessions/${sessionId}/activities`),\n\n /** Create a new activitie */\n create: (\n sessionId: string,\n data: CreateActivityRequest\n ): Promise<SessionActivity> =>\n request(\"POST\", `/sessions/${sessionId}/activities`, data),\n\n /** Update an activitie */\n update: (\n sessionId: string,\n activityId: string,\n data: UpdateActivityRequest\n ): Promise<SessionActivity> =>\n request(\n \"PATCH\",\n `/sessions/${sessionId}/activities/${activityId}`,\n data\n ),\n\n /** Delete an activitie */\n delete: (\n sessionId: string,\n activityId: string\n ): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/sessions/${sessionId}/activities/${activityId}`),\n },\n },\n\n // ============ Webhooks ============\n webhooks: {\n /** List all webhooks */\n list: (\n params: WebhookListParams = {}\n ): Promise<PaginatedResponse<Webhook>> =>\n request(\"GET\", `/webhooks${buildQueryString(params)}`),\n\n /** Get a single webhook by ID */\n get: (id: string): Promise<Webhook> => request(\"GET\", `/webhooks/${id}`),\n\n /** Create a new webhook */\n create: (data: CreateWebhookRequest): Promise<WebhookWithSecret> =>\n request(\"POST\", \"/webhooks\", data),\n\n /** Update an existing webhook */\n update: (id: string, data: UpdateWebhookRequest): Promise<Webhook> =>\n request(\"PATCH\", `/webhooks/${id}`, data),\n\n /** Delete a webhook */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/webhooks/${id}`),\n\n /** Rotate webhook secret */\n rotateSecret: (id: string): Promise<{ secret: string }> =>\n request(\"POST\", `/webhooks/${id}/rotate-secret`),\n\n /** List deliveries for a webhook */\n deliveries: (\n id: string,\n params: PaginationParams = {}\n ): Promise<PaginatedResponse<WebhookDelivery>> =>\n request(\"GET\", `/webhooks/${id}/deliveries${buildQueryString(params)}`),\n },\n }\n}\n\nexport type LocuClient = ReturnType<typeof createLocuClient>\n","import { createHmac, timingSafeEqual } from \"crypto\"\nimport type { WebhookPayload } from \"./types\"\n\nexport type WebhookSignatureResult =\n | { valid: true }\n | { valid: false; error: string }\n\nexport type ParsedWebhookSignature = {\n timestamp: number\n signature: string\n}\n\nexport type VerifyWebhookOptions = {\n /** Maximum age of signature in seconds (default: 300 = 5 minutes) */\n maxAge?: number\n}\n\n/**\n * Parse a webhook signature header into its components.\n *\n * The signature header format is: `t=<timestamp>,v1=<hex_signature>`\n *\n * @param signatureHeader - The X-Webhook-Signature header value\n * @returns Parsed timestamp and signature, or null if invalid format\n *\n * @example\n * ```typescript\n * const parsed = parseWebhookSignature(request.headers['x-webhook-signature'])\n * if (parsed) {\n * console.log('Timestamp:', parsed.timestamp)\n * console.log('Signature:', parsed.signature)\n * }\n * ```\n */\nexport const parseWebhookSignature = (\n signatureHeader: string\n): ParsedWebhookSignature | null => {\n const parts = signatureHeader.split(\",\")\n\n let timestamp: number | null = null\n let signature: string | null = null\n\n for (const part of parts) {\n const eqIndex = part.indexOf(\"=\")\n if (eqIndex === -1) continue\n const key = part.slice(0, eqIndex)\n const value = part.slice(eqIndex + 1)\n if (key === \"t\") {\n timestamp = parseInt(value, 10)\n } else if (key === \"v1\") {\n signature = value\n }\n }\n\n if (timestamp === null || signature === null || isNaN(timestamp)) {\n return null\n }\n\n return { timestamp, signature }\n}\n\n/**\n * Verify a webhook signature using HMAC-SHA256.\n *\n * This function verifies that a webhook payload was signed by Locu using your webhook secret.\n * It also checks that the signature timestamp is not too old to prevent replay attacks.\n *\n * @param secret - Your webhook secret (starts with `whsec_`)\n * @param signatureHeader - The X-Webhook-Signature header value\n * @param body - The raw request body as a string\n * @param options - Optional verification settings\n * @returns Object with `valid: true` if valid, or `valid: false` with an error message\n *\n * @example\n * ```typescript\n * import { verifyWebhookSignature } from '@locu/api-client'\n *\n * app.post('/webhooks/locu', (req, res) => {\n * const result = verifyWebhookSignature(\n * process.env.LOCU_WEBHOOK_SECRET,\n * req.headers['x-webhook-signature'],\n * req.body, // raw body string\n * { maxAge: 300 } // 5 minutes\n * )\n *\n * if (!result.valid) {\n * return res.status(401).json({ error: result.error })\n * }\n *\n * // Process the webhook\n * const payload = JSON.parse(req.body)\n * console.log('Received event:', payload.event)\n * })\n * ```\n */\nexport const verifyWebhookSignature = (\n secret: string,\n signatureHeader: string,\n body: string,\n options?: VerifyWebhookOptions\n): WebhookSignatureResult => {\n const parsed = parseWebhookSignature(signatureHeader)\n\n if (!parsed) {\n return { valid: false, error: \"Invalid signature format\" }\n }\n\n const { timestamp, signature } = parsed\n\n // Check timestamp age if maxAge is specified\n if (options?.maxAge !== undefined) {\n const now = Math.floor(Date.now() / 1000)\n const age = now - timestamp\n\n if (age > options.maxAge) {\n return { valid: false, error: \"Signature timestamp too old\" }\n }\n\n if (age < -60) {\n // Allow 1 minute clock skew into the future\n return { valid: false, error: \"Signature timestamp in the future\" }\n }\n }\n\n // Compute expected signature\n const signaturePayload = `${timestamp}.${body}`\n const expectedSignature = createHmac(\"sha256\", secret)\n .update(signaturePayload)\n .digest(\"hex\")\n\n // Use timing-safe comparison to prevent timing attacks\n const signatureBuffer = Buffer.from(signature, \"hex\")\n const expectedBuffer = Buffer.from(expectedSignature, \"hex\")\n\n if (signatureBuffer.length !== expectedBuffer.length) {\n return { valid: false, error: \"Invalid signature\" }\n }\n\n const isValid = timingSafeEqual(signatureBuffer, expectedBuffer)\n\n if (!isValid) {\n return { valid: false, error: \"Invalid signature\" }\n }\n\n return { valid: true }\n}\n\n/**\n * Parse a webhook payload from a JSON string.\n *\n * @param body - The raw request body as a JSON string\n * @returns The parsed webhook payload\n *\n * @example\n * ```typescript\n * import { parseWebhookPayload, TaskWebhookPayload } from '@locu/api-client'\n *\n * const payload = parseWebhookPayload<TaskWebhookPayload>(req.body)\n * console.log('Event:', payload.event) // e.g., \"task.created\"\n * console.log('Task name:', payload.data.name)\n * ```\n */\nexport const parseWebhookPayload = <T = unknown>(\n body: string\n): WebhookPayload<T> => {\n return JSON.parse(body) as WebhookPayload<T>\n}\n\n/**\n * Generate a webhook signature for testing purposes.\n *\n * This is useful for testing your webhook handlers locally.\n *\n * @param secret - Your webhook secret\n * @param timestamp - Unix timestamp in seconds\n * @param body - The request body as a string\n * @returns The signature header value in format `t=<timestamp>,v1=<signature>`\n *\n * @example\n * ```typescript\n * import { generateWebhookSignature } from '@locu/api-client'\n *\n * const body = JSON.stringify({ event: 'task.created', timestamp: '...', data: {...} })\n * const signature = generateWebhookSignature('whsec_...', Math.floor(Date.now() / 1000), body)\n * // Use signature for testing your webhook handler\n * ```\n */\nexport const generateWebhookSignature = (\n secret: string,\n timestamp: number,\n body: string\n): string => {\n const signaturePayload = `${timestamp}.${body}`\n const signature = createHmac(\"sha256\", secret)\n .update(signaturePayload)\n .digest(\"hex\")\n return `t=${timestamp},v1=${signature}`\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/webhook.ts"],"sourcesContent":["// Client\nexport { createLocuClient, LocuApiError } from \"./client\"\nexport type { LocuClientConfig, LocuClient } from \"./client\"\n\n// Webhook utilities\nexport {\n verifyWebhookSignature,\n parseWebhookSignature,\n parseWebhookPayload,\n generateWebhookSignature,\n} from \"./webhook\"\nexport type {\n WebhookSignatureResult,\n ParsedWebhookSignature,\n VerifyWebhookOptions,\n} from \"./webhook\"\n\nexport type * from \"./generated/api\"\n","import type {\n ActivityListParams,\n ApiError,\n CreateActivityRequest,\n CreateNoteRequest,\n CreateProjectRequest,\n CreateSessionRequest,\n CreateTaskRequest,\n CreateWebhookRequest,\n MeResponse,\n Note,\n NoteListParams,\n PaginatedResponse,\n PaginationParams,\n Project,\n ProjectListParams,\n Session,\n SessionActivity,\n SessionListParams,\n SessionWithActivities,\n StartTimerRequest,\n StopTimerResponse,\n SubtaskListParams,\n Task,\n TaskDetailResponse,\n TaskListParams,\n TaskSectionsParams,\n TaskSectionsResponse,\n TimerState,\n UpdateActivityRequest,\n UpdateNoteRequest,\n UpdateProjectRequest,\n UpdateSessionRequest,\n UpdateTaskRequest,\n UpdateWebhookRequest,\n Webhook,\n WebhookDelivery,\n WebhookListParams,\n WebhookWithSecret,\n} from \"./types\"\n\nexport type LocuClientConfig = {\n /** API base URL (defaults to https://api.locu.app/api/v1) */\n baseUrl?: string\n /** Personal Access Token for authentication */\n token: string\n /** Custom fetch implementation (defaults to global fetch) */\n fetch?: typeof fetch\n}\n\nexport class LocuApiError extends Error {\n status: number\n code?: string\n\n constructor(message: string, status: number, code?: string) {\n super(message)\n this.name = \"LocuApiError\"\n this.status = status\n this.code = code\n }\n}\n\nconst buildQueryString = (params: Record<string, unknown>): string => {\n const searchParams = new URLSearchParams()\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== null) {\n searchParams.set(key, String(value))\n }\n }\n const qs = searchParams.toString()\n return qs ? `?${qs}` : \"\"\n}\n\nexport const createLocuClient = (config: LocuClientConfig) => {\n const baseUrl = config.baseUrl || \"https://api.locu.app/api/v1\"\n const fetchFn = config.fetch || fetch\n\n const request = async <T>(\n method: string,\n path: string,\n body?: unknown\n ): Promise<T> => {\n const url = `${baseUrl}${path}`\n const headers: Record<string, string> = {\n Authorization: `Bearer ${config.token}`,\n \"Content-Type\": \"application/json\",\n }\n\n const response = await fetchFn(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n })\n\n if (!response.ok) {\n let errorData: ApiError | null = null\n try {\n errorData = await response.json()\n } catch {\n // Ignore JSON parse errors\n }\n throw new LocuApiError(\n errorData?.message || `Request failed with status ${response.status}`,\n response.status,\n errorData?.code\n )\n }\n\n // Handle 204 No Content\n if (response.status === 204) {\n return undefined as T\n }\n\n return response.json()\n }\n\n return {\n // ============ Me ============\n me: {\n /** Get current me */\n get: (): Promise<MeResponse> => request(\"GET\", \"/me\"),\n },\n\n // ============ Timer ============\n timer: {\n /** Get current timer */\n get: (): Promise<TimerState> => request(\"GET\", \"/timer\"),\n\n /** Start a new timer */\n start: (data?: StartTimerRequest): Promise<TimerState> =>\n request(\"POST\", \"/timer/start\", data),\n\n /** Pause the running timer */\n pause: (): Promise<TimerState> => request(\"POST\", \"/timer/pause\"),\n\n /** Resume a paused timer */\n continue: (): Promise<TimerState> => request(\"POST\", \"/timer/continue\"),\n\n /** Stop timer and save sessions */\n stop: (): Promise<StopTimerResponse> => request(\"POST\", \"/timer/stop\"),\n\n /** Cancel timer without saving sessions */\n cancel: (): Promise<TimerState> => request(\"DELETE\", \"/timer\"),\n },\n\n // ============ Tasks ============\n tasks: {\n /** List all tasks */\n list: (params: TaskListParams = {}): Promise<PaginatedResponse<Task>> =>\n request(\"GET\", `/tasks${buildQueryString(params)}`),\n\n /** Get a single task by ID */\n get: (id: string): Promise<TaskDetailResponse> =>\n request(\"GET\", `/tasks/${id}`),\n\n /** Create a new task */\n create: (data: CreateTaskRequest): Promise<Task> =>\n request(\"POST\", \"/tasks\", data),\n\n /** Update an existing task */\n update: (id: string, data: UpdateTaskRequest): Promise<Task> =>\n request(\"PATCH\", `/tasks/${id}`, data),\n\n /** Delete a task */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/tasks/${id}`),\n\n /** Get tasks organized by section (today, sooner, later) */\n sections: (\n params: TaskSectionsParams = {}\n ): Promise<TaskSectionsResponse> =>\n request(\"GET\", `/tasks/sections${buildQueryString(params)}`),\n\n /** List subtasks for a task */\n subtasks: (\n id: string,\n params: SubtaskListParams = {}\n ): Promise<PaginatedResponse<Task>> =>\n request(\"GET\", `/tasks/${id}/subtasks${buildQueryString(params)}`),\n\n /** Create a subtask under a parent task */\n createSubtask: (\n parentId: string,\n data: Omit<CreateTaskRequest, \"parentId\" | \"section\">\n ): Promise<Task> => request(\"POST\", \"/tasks\", { ...data, parentId }),\n },\n\n // ============ Projects ============\n projects: {\n /** List all projects */\n list: (\n params: ProjectListParams = {}\n ): Promise<PaginatedResponse<Project>> =>\n request(\"GET\", `/projects${buildQueryString(params)}`),\n\n /** Get a single project by ID */\n get: (id: string): Promise<Project> => request(\"GET\", `/projects/${id}`),\n\n /** Create a new project */\n create: (data: CreateProjectRequest): Promise<Project> =>\n request(\"POST\", \"/projects\", data),\n\n /** Update an existing project */\n update: (id: string, data: UpdateProjectRequest): Promise<Project> =>\n request(\"PATCH\", `/projects/${id}`, data),\n\n /** Delete a project */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/projects/${id}`),\n },\n\n // ============ Notes ============\n notes: {\n /** List all notes */\n list: (params: NoteListParams = {}): Promise<PaginatedResponse<Note>> =>\n request(\"GET\", `/notes${buildQueryString(params)}`),\n\n /** Get a single note by ID */\n get: (id: string): Promise<Note> => request(\"GET\", `/notes/${id}`),\n\n /** Create a new note */\n create: (data: CreateNoteRequest): Promise<Note> =>\n request(\"POST\", \"/notes\", data),\n\n /** Update an existing note */\n update: (id: string, data: UpdateNoteRequest): Promise<Note> =>\n request(\"PATCH\", `/notes/${id}`, data),\n\n /** Delete a note */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/notes/${id}`),\n },\n\n // ============ Sessions ============\n sessions: {\n /** List all sessions */\n list: (\n params: SessionListParams = {}\n ): Promise<PaginatedResponse<SessionWithActivities>> =>\n request(\"GET\", `/sessions${buildQueryString(params)}`),\n\n /** Get a single session by ID */\n get: (id: string): Promise<Session> => request(\"GET\", `/sessions/${id}`),\n\n /** Create a new session */\n create: (data: CreateSessionRequest): Promise<Session> =>\n request(\"POST\", \"/sessions\", data),\n\n /** Update an existing session */\n update: (id: string, data: UpdateSessionRequest): Promise<Session> =>\n request(\"PATCH\", `/sessions/${id}`, data),\n\n /** Delete a session */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/sessions/${id}`),\n\n /** List all activities with optional filters */\n listActivities: (\n params: ActivityListParams = {}\n ): Promise<PaginatedResponse<SessionActivity>> =>\n request(\"GET\", `/sessions/activities${buildQueryString(params)}`),\n\n // Activities\n activities: {\n /** List activities for a session */\n list: (sessionId: string): Promise<{ data: SessionActivity[] }> =>\n request(\"GET\", `/sessions/${sessionId}/activities`),\n\n /** Create a new activitie */\n create: (\n sessionId: string,\n data: CreateActivityRequest\n ): Promise<SessionActivity> =>\n request(\"POST\", `/sessions/${sessionId}/activities`, data),\n\n /** Update an activitie */\n update: (\n sessionId: string,\n activityId: string,\n data: UpdateActivityRequest\n ): Promise<SessionActivity> =>\n request(\n \"PATCH\",\n `/sessions/${sessionId}/activities/${activityId}`,\n data\n ),\n\n /** Delete an activitie */\n delete: (\n sessionId: string,\n activityId: string\n ): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/sessions/${sessionId}/activities/${activityId}`),\n },\n },\n\n // ============ Webhooks ============\n webhooks: {\n /** List all webhooks */\n list: (\n params: WebhookListParams = {}\n ): Promise<PaginatedResponse<Webhook>> =>\n request(\"GET\", `/webhooks${buildQueryString(params)}`),\n\n /** Get a single webhook by ID */\n get: (id: string): Promise<Webhook> => request(\"GET\", `/webhooks/${id}`),\n\n /** Create a new webhook */\n create: (data: CreateWebhookRequest): Promise<WebhookWithSecret> =>\n request(\"POST\", \"/webhooks\", data),\n\n /** Update an existing webhook */\n update: (id: string, data: UpdateWebhookRequest): Promise<Webhook> =>\n request(\"PATCH\", `/webhooks/${id}`, data),\n\n /** Delete a webhook */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/webhooks/${id}`),\n\n /** Rotate webhook secret */\n rotateSecret: (id: string): Promise<{ secret: string }> =>\n request(\"POST\", `/webhooks/${id}/rotate-secret`),\n\n /** List deliveries for a webhook */\n deliveries: (\n id: string,\n params: PaginationParams = {}\n ): Promise<PaginatedResponse<WebhookDelivery>> =>\n request(\"GET\", `/webhooks/${id}/deliveries${buildQueryString(params)}`),\n },\n }\n}\n\nexport type LocuClient = ReturnType<typeof createLocuClient>\n","import { createHmac, timingSafeEqual } from \"crypto\"\nimport type { WebhookPayload } from \"./types\"\n\nexport type WebhookSignatureResult =\n | { valid: true }\n | { valid: false; error: string }\n\nexport type ParsedWebhookSignature = {\n timestamp: number\n signature: string\n}\n\nexport type VerifyWebhookOptions = {\n /** Maximum age of signature in seconds (default: 300 = 5 minutes) */\n maxAge?: number\n}\n\n/**\n * Parse a webhook signature header into its components.\n *\n * The signature header format is: `t=<timestamp>,v1=<hex_signature>`\n *\n * @param signatureHeader - The X-Webhook-Signature header value\n * @returns Parsed timestamp and signature, or null if invalid format\n *\n * @example\n * ```typescript\n * const parsed = parseWebhookSignature(request.headers['x-webhook-signature'])\n * if (parsed) {\n * console.log('Timestamp:', parsed.timestamp)\n * console.log('Signature:', parsed.signature)\n * }\n * ```\n */\nexport const parseWebhookSignature = (\n signatureHeader: string\n): ParsedWebhookSignature | null => {\n const parts = signatureHeader.split(\",\")\n\n let timestamp: number | null = null\n let signature: string | null = null\n\n for (const part of parts) {\n const eqIndex = part.indexOf(\"=\")\n if (eqIndex === -1) continue\n const key = part.slice(0, eqIndex)\n const value = part.slice(eqIndex + 1)\n if (key === \"t\") {\n timestamp = parseInt(value, 10)\n } else if (key === \"v1\") {\n signature = value\n }\n }\n\n if (timestamp === null || signature === null || isNaN(timestamp)) {\n return null\n }\n\n return { timestamp, signature }\n}\n\n/**\n * Verify a webhook signature using HMAC-SHA256.\n *\n * This function verifies that a webhook payload was signed by Locu using your webhook secret.\n * It also checks that the signature timestamp is not too old to prevent replay attacks.\n *\n * @param secret - Your webhook secret (starts with `whsec_`)\n * @param signatureHeader - The X-Webhook-Signature header value\n * @param body - The raw request body as a string\n * @param options - Optional verification settings\n * @returns Object with `valid: true` if valid, or `valid: false` with an error message\n *\n * @example\n * ```typescript\n * import { verifyWebhookSignature } from '@locu/api-client'\n *\n * app.post('/webhooks/locu', (req, res) => {\n * const result = verifyWebhookSignature(\n * process.env.LOCU_WEBHOOK_SECRET,\n * req.headers['x-webhook-signature'],\n * req.body, // raw body string\n * { maxAge: 300 } // 5 minutes\n * )\n *\n * if (!result.valid) {\n * return res.status(401).json({ error: result.error })\n * }\n *\n * // Process the webhook\n * const payload = JSON.parse(req.body)\n * console.log('Received event:', payload.event)\n * })\n * ```\n */\nexport const verifyWebhookSignature = (\n secret: string,\n signatureHeader: string,\n body: string,\n options?: VerifyWebhookOptions\n): WebhookSignatureResult => {\n const parsed = parseWebhookSignature(signatureHeader)\n\n if (!parsed) {\n return { valid: false, error: \"Invalid signature format\" }\n }\n\n const { timestamp, signature } = parsed\n\n // Check timestamp age if maxAge is specified\n if (options?.maxAge !== undefined) {\n const now = Math.floor(Date.now() / 1000)\n const age = now - timestamp\n\n if (age > options.maxAge) {\n return { valid: false, error: \"Signature timestamp too old\" }\n }\n\n if (age < -60) {\n // Allow 1 minute clock skew into the future\n return { valid: false, error: \"Signature timestamp in the future\" }\n }\n }\n\n // Compute expected signature\n const signaturePayload = `${timestamp}.${body}`\n const expectedSignature = createHmac(\"sha256\", secret)\n .update(signaturePayload)\n .digest(\"hex\")\n\n // Use timing-safe comparison to prevent timing attacks\n const signatureBuffer = Buffer.from(signature, \"hex\")\n const expectedBuffer = Buffer.from(expectedSignature, \"hex\")\n\n if (signatureBuffer.length !== expectedBuffer.length) {\n return { valid: false, error: \"Invalid signature\" }\n }\n\n const isValid = timingSafeEqual(signatureBuffer, expectedBuffer)\n\n if (!isValid) {\n return { valid: false, error: \"Invalid signature\" }\n }\n\n return { valid: true }\n}\n\n/**\n * Parse a webhook payload from a JSON string.\n *\n * @param body - The raw request body as a JSON string\n * @returns The parsed webhook payload\n *\n * @example\n * ```typescript\n * import { parseWebhookPayload, TaskWebhookPayload } from '@locu/api-client'\n *\n * const payload = parseWebhookPayload<TaskWebhookPayload>(req.body)\n * console.log('Event:', payload.event) // e.g., \"task.created\"\n * console.log('Task name:', payload.data.name)\n * ```\n */\nexport const parseWebhookPayload = <T = unknown>(\n body: string\n): WebhookPayload<T> => {\n return JSON.parse(body) as WebhookPayload<T>\n}\n\n/**\n * Generate a webhook signature for testing purposes.\n *\n * This is useful for testing your webhook handlers locally.\n *\n * @param secret - Your webhook secret\n * @param timestamp - Unix timestamp in seconds\n * @param body - The request body as a string\n * @returns The signature header value in format `t=<timestamp>,v1=<signature>`\n *\n * @example\n * ```typescript\n * import { generateWebhookSignature } from '@locu/api-client'\n *\n * const body = JSON.stringify({ event: 'task.created', timestamp: '...', data: {...} })\n * const signature = generateWebhookSignature('whsec_...', Math.floor(Date.now() / 1000), body)\n * // Use signature for testing your webhook handler\n * ```\n */\nexport const generateWebhookSignature = (\n secret: string,\n timestamp: number,\n body: string\n): string => {\n const signaturePayload = `${timestamp}.${body}`\n const signature = createHmac(\"sha256\", secret)\n .update(signaturePayload)\n .digest(\"hex\")\n return `t=${timestamp},v1=${signature}`\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/webhook.ts"],"sourcesContent":["import type {\n ActivityListParams,\n ApiError,\n CreateActivityRequest,\n CreateNoteRequest,\n CreateProjectRequest,\n CreateSessionRequest,\n CreateTaskRequest,\n CreateWebhookRequest,\n MeResponse,\n Note,\n NoteListParams,\n PaginatedResponse,\n PaginationParams,\n Project,\n ProjectListParams,\n Session,\n SessionActivity,\n SessionListParams,\n SessionWithActivities,\n StartTimerRequest,\n StopTimerResponse,\n SubtaskListParams,\n Task,\n TaskListParams,\n TaskSectionsParams,\n TaskSectionsResponse,\n TimerState,\n UpdateActivityRequest,\n UpdateNoteRequest,\n UpdateProjectRequest,\n UpdateSessionRequest,\n UpdateTaskRequest,\n UpdateWebhookRequest,\n Webhook,\n WebhookDelivery,\n WebhookListParams,\n WebhookWithSecret,\n} from \"./types\"\n\nexport type LocuClientConfig = {\n /** API base URL (defaults to https://api.locu.app/api/v1) */\n baseUrl?: string\n /** Personal Access Token for authentication */\n token: string\n /** Custom fetch implementation (defaults to global fetch) */\n fetch?: typeof fetch\n}\n\nexport class LocuApiError extends Error {\n status: number\n code?: string\n\n constructor(message: string, status: number, code?: string) {\n super(message)\n this.name = \"LocuApiError\"\n this.status = status\n this.code = code\n }\n}\n\nconst buildQueryString = (params: Record<string, unknown>): string => {\n const searchParams = new URLSearchParams()\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== null) {\n searchParams.set(key, String(value))\n }\n }\n const qs = searchParams.toString()\n return qs ? `?${qs}` : \"\"\n}\n\nexport const createLocuClient = (config: LocuClientConfig) => {\n const baseUrl = config.baseUrl || \"https://api.locu.app/api/v1\"\n const fetchFn = config.fetch || fetch\n\n const request = async <T>(\n method: string,\n path: string,\n body?: unknown\n ): Promise<T> => {\n const url = `${baseUrl}${path}`\n const headers: Record<string, string> = {\n Authorization: `Bearer ${config.token}`,\n \"Content-Type\": \"application/json\",\n }\n\n const response = await fetchFn(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n })\n\n if (!response.ok) {\n let errorData: ApiError | null = null\n try {\n errorData = await response.json()\n } catch {\n // Ignore JSON parse errors\n }\n throw new LocuApiError(\n errorData?.message || `Request failed with status ${response.status}`,\n response.status,\n errorData?.code\n )\n }\n\n // Handle 204 No Content\n if (response.status === 204) {\n return undefined as T\n }\n\n return response.json()\n }\n\n return {\n // ============ Me ============\n me: {\n /** Get current me */\n get: (): Promise<MeResponse> => request(\"GET\", \"/me\"),\n },\n\n // ============ Timer ============\n timer: {\n /** Get current timer */\n get: (): Promise<TimerState> => request(\"GET\", \"/timer\"),\n\n /** Start a new timer */\n start: (data?: StartTimerRequest): Promise<TimerState> =>\n request(\"POST\", \"/timer/start\", data),\n\n /** Pause the running timer */\n pause: (): Promise<TimerState> => request(\"POST\", \"/timer/pause\"),\n\n /** Resume a paused timer */\n continue: (): Promise<TimerState> => request(\"POST\", \"/timer/continue\"),\n\n /** Stop timer and save sessions */\n stop: (): Promise<StopTimerResponse> => request(\"POST\", \"/timer/stop\"),\n\n /** Cancel timer without saving sessions */\n cancel: (): Promise<TimerState> => request(\"DELETE\", \"/timer\"),\n },\n\n // ============ Tasks ============\n tasks: {\n /** List all tasks */\n list: (params: TaskListParams = {}): Promise<PaginatedResponse<Task>> =>\n request(\"GET\", `/tasks${buildQueryString(params)}`),\n\n /** Get a single task by ID */\n get: (id: string): Promise<Task> => request(\"GET\", `/tasks/${id}`),\n\n /** Create a new task */\n create: (data: CreateTaskRequest): Promise<Task> =>\n request(\"POST\", \"/tasks\", data),\n\n /** Update an existing task */\n update: (id: string, data: UpdateTaskRequest): Promise<Task> =>\n request(\"PATCH\", `/tasks/${id}`, data),\n\n /** Delete a task */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/tasks/${id}`),\n\n /** Get tasks organized by section (today, sooner, later) */\n sections: (\n params: TaskSectionsParams = {}\n ): Promise<TaskSectionsResponse> =>\n request(\"GET\", `/tasks/sections${buildQueryString(params)}`),\n\n /** List subtasks for a task */\n subtasks: (\n id: string,\n params: SubtaskListParams = {}\n ): Promise<PaginatedResponse<Task>> =>\n request(\"GET\", `/tasks/${id}/subtasks${buildQueryString(params)}`),\n\n /** Create a subtask under a parent task */\n createSubtask: (\n parentId: string,\n data: Omit<CreateTaskRequest, \"parentId\" | \"section\">\n ): Promise<Task> => request(\"POST\", \"/tasks\", { ...data, parentId }),\n },\n\n // ============ Projects ============\n projects: {\n /** List all projects */\n list: (\n params: ProjectListParams = {}\n ): Promise<PaginatedResponse<Project>> =>\n request(\"GET\", `/projects${buildQueryString(params)}`),\n\n /** Get a single project by ID */\n get: (id: string): Promise<Project> => request(\"GET\", `/projects/${id}`),\n\n /** Create a new project */\n create: (data: CreateProjectRequest): Promise<Project> =>\n request(\"POST\", \"/projects\", data),\n\n /** Update an existing project */\n update: (id: string, data: UpdateProjectRequest): Promise<Project> =>\n request(\"PATCH\", `/projects/${id}`, data),\n\n /** Delete a project */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/projects/${id}`),\n },\n\n // ============ Notes ============\n notes: {\n /** List all notes */\n list: (params: NoteListParams = {}): Promise<PaginatedResponse<Note>> =>\n request(\"GET\", `/notes${buildQueryString(params)}`),\n\n /** Get a single note by ID */\n get: (id: string): Promise<Note> => request(\"GET\", `/notes/${id}`),\n\n /** Create a new note */\n create: (data: CreateNoteRequest): Promise<Note> =>\n request(\"POST\", \"/notes\", data),\n\n /** Update an existing note */\n update: (id: string, data: UpdateNoteRequest): Promise<Note> =>\n request(\"PATCH\", `/notes/${id}`, data),\n\n /** Delete a note */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/notes/${id}`),\n },\n\n // ============ Sessions ============\n sessions: {\n /** List all sessions */\n list: (\n params: SessionListParams = {}\n ): Promise<PaginatedResponse<SessionWithActivities>> =>\n request(\"GET\", `/sessions${buildQueryString(params)}`),\n\n /** Get a single session by ID */\n get: (id: string): Promise<SessionWithActivities> =>\n request(\"GET\", `/sessions/${id}`),\n\n /** Create a new session */\n create: (data: CreateSessionRequest): Promise<Session> =>\n request(\"POST\", \"/sessions\", data),\n\n /** Update an existing session */\n update: (id: string, data: UpdateSessionRequest): Promise<Session> =>\n request(\"PATCH\", `/sessions/${id}`, data),\n\n /** Delete a session */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/sessions/${id}`),\n\n /** List all activities with optional filters */\n listActivities: (\n params: ActivityListParams = {}\n ): Promise<PaginatedResponse<SessionActivity>> =>\n request(\"GET\", `/sessions/activities${buildQueryString(params)}`),\n\n // Activities\n activities: {\n /** List activities for a session */\n list: (sessionId: string): Promise<{ data: SessionActivity[] }> =>\n request(\"GET\", `/sessions/${sessionId}/activities`),\n\n /** Create a new activitie */\n create: (\n sessionId: string,\n data: CreateActivityRequest\n ): Promise<SessionActivity> =>\n request(\"POST\", `/sessions/${sessionId}/activities`, data),\n\n /** Update an activitie */\n update: (\n sessionId: string,\n activityId: string,\n data: UpdateActivityRequest\n ): Promise<SessionActivity> =>\n request(\n \"PATCH\",\n `/sessions/${sessionId}/activities/${activityId}`,\n data\n ),\n\n /** Delete an activitie */\n delete: (\n sessionId: string,\n activityId: string\n ): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/sessions/${sessionId}/activities/${activityId}`),\n },\n },\n\n // ============ Webhooks ============\n webhooks: {\n /** List all webhooks */\n list: (\n params: WebhookListParams = {}\n ): Promise<PaginatedResponse<Webhook>> =>\n request(\"GET\", `/webhooks${buildQueryString(params)}`),\n\n /** Get a single webhook by ID */\n get: (id: string): Promise<Webhook> => request(\"GET\", `/webhooks/${id}`),\n\n /** Create a new webhook */\n create: (data: CreateWebhookRequest): Promise<WebhookWithSecret> =>\n request(\"POST\", \"/webhooks\", data),\n\n /** Update an existing webhook */\n update: (id: string, data: UpdateWebhookRequest): Promise<Webhook> =>\n request(\"PATCH\", `/webhooks/${id}`, data),\n\n /** Delete a webhook */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/webhooks/${id}`),\n\n /** Rotate webhook secret */\n rotateSecret: (id: string): Promise<{ secret: string }> =>\n request(\"POST\", `/webhooks/${id}/rotate-secret`),\n\n /** List deliveries for a webhook */\n deliveries: (\n id: string,\n params: PaginationParams = {}\n ): Promise<PaginatedResponse<WebhookDelivery>> =>\n request(\"GET\", `/webhooks/${id}/deliveries${buildQueryString(params)}`),\n },\n }\n}\n\nexport type LocuClient = ReturnType<typeof createLocuClient>\n","import { createHmac, timingSafeEqual } from \"crypto\"\nimport type { WebhookPayload } from \"./types\"\n\nexport type WebhookSignatureResult =\n | { valid: true }\n | { valid: false; error: string }\n\nexport type ParsedWebhookSignature = {\n timestamp: number\n signature: string\n}\n\nexport type VerifyWebhookOptions = {\n /** Maximum age of signature in seconds (default: 300 = 5 minutes) */\n maxAge?: number\n}\n\n/**\n * Parse a webhook signature header into its components.\n *\n * The signature header format is: `t=<timestamp>,v1=<hex_signature>`\n *\n * @param signatureHeader - The X-Webhook-Signature header value\n * @returns Parsed timestamp and signature, or null if invalid format\n *\n * @example\n * ```typescript\n * const parsed = parseWebhookSignature(request.headers['x-webhook-signature'])\n * if (parsed) {\n * console.log('Timestamp:', parsed.timestamp)\n * console.log('Signature:', parsed.signature)\n * }\n * ```\n */\nexport const parseWebhookSignature = (\n signatureHeader: string\n): ParsedWebhookSignature | null => {\n const parts = signatureHeader.split(\",\")\n\n let timestamp: number | null = null\n let signature: string | null = null\n\n for (const part of parts) {\n const eqIndex = part.indexOf(\"=\")\n if (eqIndex === -1) continue\n const key = part.slice(0, eqIndex)\n const value = part.slice(eqIndex + 1)\n if (key === \"t\") {\n timestamp = parseInt(value, 10)\n } else if (key === \"v1\") {\n signature = value\n }\n }\n\n if (timestamp === null || signature === null || isNaN(timestamp)) {\n return null\n }\n\n return { timestamp, signature }\n}\n\n/**\n * Verify a webhook signature using HMAC-SHA256.\n *\n * This function verifies that a webhook payload was signed by Locu using your webhook secret.\n * It also checks that the signature timestamp is not too old to prevent replay attacks.\n *\n * @param secret - Your webhook secret (starts with `whsec_`)\n * @param signatureHeader - The X-Webhook-Signature header value\n * @param body - The raw request body as a string\n * @param options - Optional verification settings\n * @returns Object with `valid: true` if valid, or `valid: false` with an error message\n *\n * @example\n * ```typescript\n * import { verifyWebhookSignature } from '@locu/api-client'\n *\n * app.post('/webhooks/locu', (req, res) => {\n * const result = verifyWebhookSignature(\n * process.env.LOCU_WEBHOOK_SECRET,\n * req.headers['x-webhook-signature'],\n * req.body, // raw body string\n * { maxAge: 300 } // 5 minutes\n * )\n *\n * if (!result.valid) {\n * return res.status(401).json({ error: result.error })\n * }\n *\n * // Process the webhook\n * const payload = JSON.parse(req.body)\n * console.log('Received event:', payload.event)\n * })\n * ```\n */\nexport const verifyWebhookSignature = (\n secret: string,\n signatureHeader: string,\n body: string,\n options?: VerifyWebhookOptions\n): WebhookSignatureResult => {\n const parsed = parseWebhookSignature(signatureHeader)\n\n if (!parsed) {\n return { valid: false, error: \"Invalid signature format\" }\n }\n\n const { timestamp, signature } = parsed\n\n // Check timestamp age if maxAge is specified\n if (options?.maxAge !== undefined) {\n const now = Math.floor(Date.now() / 1000)\n const age = now - timestamp\n\n if (age > options.maxAge) {\n return { valid: false, error: \"Signature timestamp too old\" }\n }\n\n if (age < -60) {\n // Allow 1 minute clock skew into the future\n return { valid: false, error: \"Signature timestamp in the future\" }\n }\n }\n\n // Compute expected signature\n const signaturePayload = `${timestamp}.${body}`\n const expectedSignature = createHmac(\"sha256\", secret)\n .update(signaturePayload)\n .digest(\"hex\")\n\n // Use timing-safe comparison to prevent timing attacks\n const signatureBuffer = Buffer.from(signature, \"hex\")\n const expectedBuffer = Buffer.from(expectedSignature, \"hex\")\n\n if (signatureBuffer.length !== expectedBuffer.length) {\n return { valid: false, error: \"Invalid signature\" }\n }\n\n const isValid = timingSafeEqual(signatureBuffer, expectedBuffer)\n\n if (!isValid) {\n return { valid: false, error: \"Invalid signature\" }\n }\n\n return { valid: true }\n}\n\n/**\n * Parse a webhook payload from a JSON string.\n *\n * @param body - The raw request body as a JSON string\n * @returns The parsed webhook payload\n *\n * @example\n * ```typescript\n * import { parseWebhookPayload, TaskWebhookPayload } from '@locu/api-client'\n *\n * const payload = parseWebhookPayload<TaskWebhookPayload>(req.body)\n * console.log('Event:', payload.event) // e.g., \"task.created\"\n * console.log('Task name:', payload.data.name)\n * ```\n */\nexport const parseWebhookPayload = <T = unknown>(\n body: string\n): WebhookPayload<T> => {\n return JSON.parse(body) as WebhookPayload<T>\n}\n\n/**\n * Generate a webhook signature for testing purposes.\n *\n * This is useful for testing your webhook handlers locally.\n *\n * @param secret - Your webhook secret\n * @param timestamp - Unix timestamp in seconds\n * @param body - The request body as a string\n * @returns The signature header value in format `t=<timestamp>,v1=<signature>`\n *\n * @example\n * ```typescript\n * import { generateWebhookSignature } from '@locu/api-client'\n *\n * const body = JSON.stringify({ event: 'task.created', timestamp: '...', data: {...} })\n * const signature = generateWebhookSignature('whsec_...', Math.floor(Date.now() / 1000), body)\n * // Use signature for testing your webhook handler\n * ```\n */\nexport const generateWebhookSignature = (\n secret: string,\n timestamp: number,\n body: string\n): string => {\n const signaturePayload = `${timestamp}.${body}`\n const signature = createHmac(\"sha256\", secret)\n .update(signaturePayload)\n .digest(\"hex\")\n return `t=${timestamp},v1=${signature}`\n}\n"],"mappings":";AAiDO,IAAM,eAAN,cAA2B,MAAM;AAAA,EACtC;AAAA,EACA;AAAA,EAEA,YAAY,SAAiB,QAAgB,MAAe;AAC1D,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,OAAO;AAAA,EACd;AACF;AAEA,IAAM,mBAAmB,CAAC,WAA4C;AACpE,QAAM,eAAe,IAAI,gBAAgB;AACzC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,UAAU,UAAa,UAAU,MAAM;AACzC,mBAAa,IAAI,KAAK,OAAO,KAAK,CAAC;AAAA,IACrC;AAAA,EACF;AACA,QAAM,KAAK,aAAa,SAAS;AACjC,SAAO,KAAK,IAAI,EAAE,KAAK;AACzB;AAEO,IAAM,mBAAmB,CAAC,WAA6B;AAC5D,QAAM,UAAU,OAAO,WAAW;AAClC,QAAM,UAAU,OAAO,SAAS;AAEhC,QAAM,UAAU,OACd,QACA,MACA,SACe;AACf,UAAM,MAAM,GAAG,OAAO,GAAG,IAAI;AAC7B,UAAM,UAAkC;AAAA,MACtC,eAAe,UAAU,OAAO,KAAK;AAAA,MACrC,gBAAgB;AAAA,IAClB;AAEA,UAAM,WAAW,MAAM,QAAQ,KAAK;AAAA,MAClC;AAAA,MACA;AAAA,MACA,MAAM,OAAO,KAAK,UAAU,IAAI,IAAI;AAAA,IACtC,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,UAAI,YAA6B;AACjC,UAAI;AACF,oBAAY,MAAM,SAAS,KAAK;AAAA,MAClC,QAAQ;AAAA,MAER;AACA,YAAM,IAAI;AAAA,QACR,WAAW,WAAW,8BAA8B,SAAS,MAAM;AAAA,QACnE,SAAS;AAAA,QACT,WAAW;AAAA,MACb;AAAA,IACF;AAGA,QAAI,SAAS,WAAW,KAAK;AAC3B,aAAO;AAAA,IACT;AAEA,WAAO,SAAS,KAAK;AAAA,EACvB;AAEA,SAAO;AAAA;AAAA,IAEL,IAAI;AAAA;AAAA,MAEF,KAAK,MAA2B,QAAQ,OAAO,KAAK;AAAA,IACtD;AAAA;AAAA,IAGA,OAAO;AAAA;AAAA,MAEL,KAAK,MAA2B,QAAQ,OAAO,QAAQ;AAAA;AAAA,MAGvD,OAAO,CAAC,SACN,QAAQ,QAAQ,gBAAgB,IAAI;AAAA;AAAA,MAGtC,OAAO,MAA2B,QAAQ,QAAQ,cAAc;AAAA;AAAA,MAGhE,UAAU,MAA2B,QAAQ,QAAQ,iBAAiB;AAAA;AAAA,MAGtE,MAAM,MAAkC,QAAQ,QAAQ,aAAa;AAAA;AAAA,MAGrE,QAAQ,MAA2B,QAAQ,UAAU,QAAQ;AAAA,IAC/D;AAAA;AAAA,IAGA,OAAO;AAAA;AAAA,MAEL,MAAM,CAAC,SAAyB,CAAC,MAC/B,QAAQ,OAAO,SAAS,iBAAiB,MAAM,CAAC,EAAE;AAAA;AAAA,MAGpD,KAAK,CAAC,OAA8B,QAAQ,OAAO,UAAU,EAAE,EAAE;AAAA;AAAA,MAGjE,QAAQ,CAAC,SACP,QAAQ,QAAQ,UAAU,IAAI;AAAA;AAAA,MAGhC,QAAQ,CAAC,IAAY,SACnB,QAAQ,SAAS,UAAU,EAAE,IAAI,IAAI;AAAA;AAAA,MAGvC,QAAQ,CAAC,OACP,QAAQ,UAAU,UAAU,EAAE,EAAE;AAAA;AAAA,MAGlC,UAAU,CACR,SAA6B,CAAC,MAE9B,QAAQ,OAAO,kBAAkB,iBAAiB,MAAM,CAAC,EAAE;AAAA;AAAA,MAG7D,UAAU,CACR,IACA,SAA4B,CAAC,MAE7B,QAAQ,OAAO,UAAU,EAAE,YAAY,iBAAiB,MAAM,CAAC,EAAE;AAAA;AAAA,MAGnE,eAAe,CACb,UACA,SACkB,QAAQ,QAAQ,UAAU,EAAE,GAAG,MAAM,SAAS,CAAC;AAAA,IACrE;AAAA;AAAA,IAGA,UAAU;AAAA;AAAA,MAER,MAAM,CACJ,SAA4B,CAAC,MAE7B,QAAQ,OAAO,YAAY,iBAAiB,MAAM,CAAC,EAAE;AAAA;AAAA,MAGvD,KAAK,CAAC,OAAiC,QAAQ,OAAO,aAAa,EAAE,EAAE;AAAA;AAAA,MAGvE,QAAQ,CAAC,SACP,QAAQ,QAAQ,aAAa,IAAI;AAAA;AAAA,MAGnC,QAAQ,CAAC,IAAY,SACnB,QAAQ,SAAS,aAAa,EAAE,IAAI,IAAI;AAAA;AAAA,MAG1C,QAAQ,CAAC,OACP,QAAQ,UAAU,aAAa,EAAE,EAAE;AAAA,IACvC;AAAA;AAAA,IAGA,OAAO;AAAA;AAAA,MAEL,MAAM,CAAC,SAAyB,CAAC,MAC/B,QAAQ,OAAO,SAAS,iBAAiB,MAAM,CAAC,EAAE;AAAA;AAAA,MAGpD,KAAK,CAAC,OAA8B,QAAQ,OAAO,UAAU,EAAE,EAAE;AAAA;AAAA,MAGjE,QAAQ,CAAC,SACP,QAAQ,QAAQ,UAAU,IAAI;AAAA;AAAA,MAGhC,QAAQ,CAAC,IAAY,SACnB,QAAQ,SAAS,UAAU,EAAE,IAAI,IAAI;AAAA;AAAA,MAGvC,QAAQ,CAAC,OACP,QAAQ,UAAU,UAAU,EAAE,EAAE;AAAA,IACpC;AAAA;AAAA,IAGA,UAAU;AAAA;AAAA,MAER,MAAM,CACJ,SAA4B,CAAC,MAE7B,QAAQ,OAAO,YAAY,iBAAiB,MAAM,CAAC,EAAE;AAAA;AAAA,MAGvD,KAAK,CAAC,OACJ,QAAQ,OAAO,aAAa,EAAE,EAAE;AAAA;AAAA,MAGlC,QAAQ,CAAC,SACP,QAAQ,QAAQ,aAAa,IAAI;AAAA;AAAA,MAGnC,QAAQ,CAAC,IAAY,SACnB,QAAQ,SAAS,aAAa,EAAE,IAAI,IAAI;AAAA;AAAA,MAG1C,QAAQ,CAAC,OACP,QAAQ,UAAU,aAAa,EAAE,EAAE;AAAA;AAAA,MAGrC,gBAAgB,CACd,SAA6B,CAAC,MAE9B,QAAQ,OAAO,uBAAuB,iBAAiB,MAAM,CAAC,EAAE;AAAA;AAAA,MAGlE,YAAY;AAAA;AAAA,QAEV,MAAM,CAAC,cACL,QAAQ,OAAO,aAAa,SAAS,aAAa;AAAA;AAAA,QAGpD,QAAQ,CACN,WACA,SAEA,QAAQ,QAAQ,aAAa,SAAS,eAAe,IAAI;AAAA;AAAA,QAG3D,QAAQ,CACN,WACA,YACA,SAEA;AAAA,UACE;AAAA,UACA,aAAa,SAAS,eAAe,UAAU;AAAA,UAC/C;AAAA,QACF;AAAA;AAAA,QAGF,QAAQ,CACN,WACA,eAEA,QAAQ,UAAU,aAAa,SAAS,eAAe,UAAU,EAAE;AAAA,MACvE;AAAA,IACF;AAAA;AAAA,IAGA,UAAU;AAAA;AAAA,MAER,MAAM,CACJ,SAA4B,CAAC,MAE7B,QAAQ,OAAO,YAAY,iBAAiB,MAAM,CAAC,EAAE;AAAA;AAAA,MAGvD,KAAK,CAAC,OAAiC,QAAQ,OAAO,aAAa,EAAE,EAAE;AAAA;AAAA,MAGvE,QAAQ,CAAC,SACP,QAAQ,QAAQ,aAAa,IAAI;AAAA;AAAA,MAGnC,QAAQ,CAAC,IAAY,SACnB,QAAQ,SAAS,aAAa,EAAE,IAAI,IAAI;AAAA;AAAA,MAG1C,QAAQ,CAAC,OACP,QAAQ,UAAU,aAAa,EAAE,EAAE;AAAA;AAAA,MAGrC,cAAc,CAAC,OACb,QAAQ,QAAQ,aAAa,EAAE,gBAAgB;AAAA;AAAA,MAGjD,YAAY,CACV,IACA,SAA2B,CAAC,MAE5B,QAAQ,OAAO,aAAa,EAAE,cAAc,iBAAiB,MAAM,CAAC,EAAE;AAAA,IAC1E;AAAA,EACF;AACF;;;AC1UA,SAAS,YAAY,uBAAuB;AAkCrC,IAAM,wBAAwB,CACnC,oBACkC;AAClC,QAAM,QAAQ,gBAAgB,MAAM,GAAG;AAEvC,MAAI,YAA2B;AAC/B,MAAI,YAA2B;AAE/B,aAAW,QAAQ,OAAO;AACxB,UAAM,UAAU,KAAK,QAAQ,GAAG;AAChC,QAAI,YAAY,GAAI;AACpB,UAAM,MAAM,KAAK,MAAM,GAAG,OAAO;AACjC,UAAM,QAAQ,KAAK,MAAM,UAAU,CAAC;AACpC,QAAI,QAAQ,KAAK;AACf,kBAAY,SAAS,OAAO,EAAE;AAAA,IAChC,WAAW,QAAQ,MAAM;AACvB,kBAAY;AAAA,IACd;AAAA,EACF;AAEA,MAAI,cAAc,QAAQ,cAAc,QAAQ,MAAM,SAAS,GAAG;AAChE,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,WAAW,UAAU;AAChC;AAoCO,IAAM,yBAAyB,CACpC,QACA,iBACA,MACA,YAC2B;AAC3B,QAAM,SAAS,sBAAsB,eAAe;AAEpD,MAAI,CAAC,QAAQ;AACX,WAAO,EAAE,OAAO,OAAO,OAAO,2BAA2B;AAAA,EAC3D;AAEA,QAAM,EAAE,WAAW,UAAU,IAAI;AAGjC,MAAI,SAAS,WAAW,QAAW;AACjC,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,UAAM,MAAM,MAAM;AAElB,QAAI,MAAM,QAAQ,QAAQ;AACxB,aAAO,EAAE,OAAO,OAAO,OAAO,8BAA8B;AAAA,IAC9D;AAEA,QAAI,MAAM,KAAK;AAEb,aAAO,EAAE,OAAO,OAAO,OAAO,oCAAoC;AAAA,IACpE;AAAA,EACF;AAGA,QAAM,mBAAmB,GAAG,SAAS,IAAI,IAAI;AAC7C,QAAM,oBAAoB,WAAW,UAAU,MAAM,EAClD,OAAO,gBAAgB,EACvB,OAAO,KAAK;AAGf,QAAM,kBAAkB,OAAO,KAAK,WAAW,KAAK;AACpD,QAAM,iBAAiB,OAAO,KAAK,mBAAmB,KAAK;AAE3D,MAAI,gBAAgB,WAAW,eAAe,QAAQ;AACpD,WAAO,EAAE,OAAO,OAAO,OAAO,oBAAoB;AAAA,EACpD;AAEA,QAAM,UAAU,gBAAgB,iBAAiB,cAAc;AAE/D,MAAI,CAAC,SAAS;AACZ,WAAO,EAAE,OAAO,OAAO,OAAO,oBAAoB;AAAA,EACpD;AAEA,SAAO,EAAE,OAAO,KAAK;AACvB;AAiBO,IAAM,sBAAsB,CACjC,SACsB;AACtB,SAAO,KAAK,MAAM,IAAI;AACxB;AAqBO,IAAM,2BAA2B,CACtC,QACA,WACA,SACW;AACX,QAAM,mBAAmB,GAAG,SAAS,IAAI,IAAI;AAC7C,QAAM,YAAY,WAAW,UAAU,MAAM,EAC1C,OAAO,gBAAgB,EACvB,OAAO,KAAK;AACf,SAAO,KAAK,SAAS,OAAO,SAAS;AACvC;","names":[]}
1
+ {"version":3,"sources":["../src/client.ts","../src/webhook.ts"],"sourcesContent":["import type {\n ActivityListParams,\n ApiError,\n CreateActivityRequest,\n CreateNoteRequest,\n CreateProjectRequest,\n CreateSessionRequest,\n CreateTaskRequest,\n CreateWebhookRequest,\n MeResponse,\n Note,\n NoteListParams,\n PaginatedResponse,\n PaginationParams,\n Project,\n ProjectListParams,\n Session,\n SessionActivity,\n SessionListParams,\n SessionWithActivities,\n StartTimerRequest,\n StopTimerResponse,\n SubtaskListParams,\n Task,\n TaskDetailResponse,\n TaskListParams,\n TaskSectionsParams,\n TaskSectionsResponse,\n TimerState,\n UpdateActivityRequest,\n UpdateNoteRequest,\n UpdateProjectRequest,\n UpdateSessionRequest,\n UpdateTaskRequest,\n UpdateWebhookRequest,\n Webhook,\n WebhookDelivery,\n WebhookListParams,\n WebhookWithSecret,\n} from \"./types\"\n\nexport type LocuClientConfig = {\n /** API base URL (defaults to https://api.locu.app/api/v1) */\n baseUrl?: string\n /** Personal Access Token for authentication */\n token: string\n /** Custom fetch implementation (defaults to global fetch) */\n fetch?: typeof fetch\n}\n\nexport class LocuApiError extends Error {\n status: number\n code?: string\n\n constructor(message: string, status: number, code?: string) {\n super(message)\n this.name = \"LocuApiError\"\n this.status = status\n this.code = code\n }\n}\n\nconst buildQueryString = (params: Record<string, unknown>): string => {\n const searchParams = new URLSearchParams()\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== null) {\n searchParams.set(key, String(value))\n }\n }\n const qs = searchParams.toString()\n return qs ? `?${qs}` : \"\"\n}\n\nexport const createLocuClient = (config: LocuClientConfig) => {\n const baseUrl = config.baseUrl || \"https://api.locu.app/api/v1\"\n const fetchFn = config.fetch || fetch\n\n const request = async <T>(\n method: string,\n path: string,\n body?: unknown\n ): Promise<T> => {\n const url = `${baseUrl}${path}`\n const headers: Record<string, string> = {\n Authorization: `Bearer ${config.token}`,\n \"Content-Type\": \"application/json\",\n }\n\n const response = await fetchFn(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n })\n\n if (!response.ok) {\n let errorData: ApiError | null = null\n try {\n errorData = await response.json()\n } catch {\n // Ignore JSON parse errors\n }\n throw new LocuApiError(\n errorData?.message || `Request failed with status ${response.status}`,\n response.status,\n errorData?.code\n )\n }\n\n // Handle 204 No Content\n if (response.status === 204) {\n return undefined as T\n }\n\n return response.json()\n }\n\n return {\n // ============ Me ============\n me: {\n /** Get current me */\n get: (): Promise<MeResponse> => request(\"GET\", \"/me\"),\n },\n\n // ============ Timer ============\n timer: {\n /** Get current timer */\n get: (): Promise<TimerState> => request(\"GET\", \"/timer\"),\n\n /** Start a new timer */\n start: (data?: StartTimerRequest): Promise<TimerState> =>\n request(\"POST\", \"/timer/start\", data),\n\n /** Pause the running timer */\n pause: (): Promise<TimerState> => request(\"POST\", \"/timer/pause\"),\n\n /** Resume a paused timer */\n continue: (): Promise<TimerState> => request(\"POST\", \"/timer/continue\"),\n\n /** Stop timer and save sessions */\n stop: (): Promise<StopTimerResponse> => request(\"POST\", \"/timer/stop\"),\n\n /** Cancel timer without saving sessions */\n cancel: (): Promise<TimerState> => request(\"DELETE\", \"/timer\"),\n },\n\n // ============ Tasks ============\n tasks: {\n /** List all tasks */\n list: (params: TaskListParams = {}): Promise<PaginatedResponse<Task>> =>\n request(\"GET\", `/tasks${buildQueryString(params)}`),\n\n /** Get a single task by ID */\n get: (id: string): Promise<TaskDetailResponse> =>\n request(\"GET\", `/tasks/${id}`),\n\n /** Create a new task */\n create: (data: CreateTaskRequest): Promise<Task> =>\n request(\"POST\", \"/tasks\", data),\n\n /** Update an existing task */\n update: (id: string, data: UpdateTaskRequest): Promise<Task> =>\n request(\"PATCH\", `/tasks/${id}`, data),\n\n /** Delete a task */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/tasks/${id}`),\n\n /** Get tasks organized by section (today, sooner, later) */\n sections: (\n params: TaskSectionsParams = {}\n ): Promise<TaskSectionsResponse> =>\n request(\"GET\", `/tasks/sections${buildQueryString(params)}`),\n\n /** List subtasks for a task */\n subtasks: (\n id: string,\n params: SubtaskListParams = {}\n ): Promise<PaginatedResponse<Task>> =>\n request(\"GET\", `/tasks/${id}/subtasks${buildQueryString(params)}`),\n\n /** Create a subtask under a parent task */\n createSubtask: (\n parentId: string,\n data: Omit<CreateTaskRequest, \"parentId\" | \"section\">\n ): Promise<Task> => request(\"POST\", \"/tasks\", { ...data, parentId }),\n },\n\n // ============ Projects ============\n projects: {\n /** List all projects */\n list: (\n params: ProjectListParams = {}\n ): Promise<PaginatedResponse<Project>> =>\n request(\"GET\", `/projects${buildQueryString(params)}`),\n\n /** Get a single project by ID */\n get: (id: string): Promise<Project> => request(\"GET\", `/projects/${id}`),\n\n /** Create a new project */\n create: (data: CreateProjectRequest): Promise<Project> =>\n request(\"POST\", \"/projects\", data),\n\n /** Update an existing project */\n update: (id: string, data: UpdateProjectRequest): Promise<Project> =>\n request(\"PATCH\", `/projects/${id}`, data),\n\n /** Delete a project */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/projects/${id}`),\n },\n\n // ============ Notes ============\n notes: {\n /** List all notes */\n list: (params: NoteListParams = {}): Promise<PaginatedResponse<Note>> =>\n request(\"GET\", `/notes${buildQueryString(params)}`),\n\n /** Get a single note by ID */\n get: (id: string): Promise<Note> => request(\"GET\", `/notes/${id}`),\n\n /** Create a new note */\n create: (data: CreateNoteRequest): Promise<Note> =>\n request(\"POST\", \"/notes\", data),\n\n /** Update an existing note */\n update: (id: string, data: UpdateNoteRequest): Promise<Note> =>\n request(\"PATCH\", `/notes/${id}`, data),\n\n /** Delete a note */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/notes/${id}`),\n },\n\n // ============ Sessions ============\n sessions: {\n /** List all sessions */\n list: (\n params: SessionListParams = {}\n ): Promise<PaginatedResponse<SessionWithActivities>> =>\n request(\"GET\", `/sessions${buildQueryString(params)}`),\n\n /** Get a single session by ID */\n get: (id: string): Promise<Session> => request(\"GET\", `/sessions/${id}`),\n\n /** Create a new session */\n create: (data: CreateSessionRequest): Promise<Session> =>\n request(\"POST\", \"/sessions\", data),\n\n /** Update an existing session */\n update: (id: string, data: UpdateSessionRequest): Promise<Session> =>\n request(\"PATCH\", `/sessions/${id}`, data),\n\n /** Delete a session */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/sessions/${id}`),\n\n /** List all activities with optional filters */\n listActivities: (\n params: ActivityListParams = {}\n ): Promise<PaginatedResponse<SessionActivity>> =>\n request(\"GET\", `/sessions/activities${buildQueryString(params)}`),\n\n // Activities\n activities: {\n /** List activities for a session */\n list: (sessionId: string): Promise<{ data: SessionActivity[] }> =>\n request(\"GET\", `/sessions/${sessionId}/activities`),\n\n /** Create a new activitie */\n create: (\n sessionId: string,\n data: CreateActivityRequest\n ): Promise<SessionActivity> =>\n request(\"POST\", `/sessions/${sessionId}/activities`, data),\n\n /** Update an activitie */\n update: (\n sessionId: string,\n activityId: string,\n data: UpdateActivityRequest\n ): Promise<SessionActivity> =>\n request(\n \"PATCH\",\n `/sessions/${sessionId}/activities/${activityId}`,\n data\n ),\n\n /** Delete an activitie */\n delete: (\n sessionId: string,\n activityId: string\n ): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/sessions/${sessionId}/activities/${activityId}`),\n },\n },\n\n // ============ Webhooks ============\n webhooks: {\n /** List all webhooks */\n list: (\n params: WebhookListParams = {}\n ): Promise<PaginatedResponse<Webhook>> =>\n request(\"GET\", `/webhooks${buildQueryString(params)}`),\n\n /** Get a single webhook by ID */\n get: (id: string): Promise<Webhook> => request(\"GET\", `/webhooks/${id}`),\n\n /** Create a new webhook */\n create: (data: CreateWebhookRequest): Promise<WebhookWithSecret> =>\n request(\"POST\", \"/webhooks\", data),\n\n /** Update an existing webhook */\n update: (id: string, data: UpdateWebhookRequest): Promise<Webhook> =>\n request(\"PATCH\", `/webhooks/${id}`, data),\n\n /** Delete a webhook */\n delete: (id: string): Promise<{ success: boolean }> =>\n request(\"DELETE\", `/webhooks/${id}`),\n\n /** Rotate webhook secret */\n rotateSecret: (id: string): Promise<{ secret: string }> =>\n request(\"POST\", `/webhooks/${id}/rotate-secret`),\n\n /** List deliveries for a webhook */\n deliveries: (\n id: string,\n params: PaginationParams = {}\n ): Promise<PaginatedResponse<WebhookDelivery>> =>\n request(\"GET\", `/webhooks/${id}/deliveries${buildQueryString(params)}`),\n },\n }\n}\n\nexport type LocuClient = ReturnType<typeof createLocuClient>\n","import { createHmac, timingSafeEqual } from \"crypto\"\nimport type { WebhookPayload } from \"./types\"\n\nexport type WebhookSignatureResult =\n | { valid: true }\n | { valid: false; error: string }\n\nexport type ParsedWebhookSignature = {\n timestamp: number\n signature: string\n}\n\nexport type VerifyWebhookOptions = {\n /** Maximum age of signature in seconds (default: 300 = 5 minutes) */\n maxAge?: number\n}\n\n/**\n * Parse a webhook signature header into its components.\n *\n * The signature header format is: `t=<timestamp>,v1=<hex_signature>`\n *\n * @param signatureHeader - The X-Webhook-Signature header value\n * @returns Parsed timestamp and signature, or null if invalid format\n *\n * @example\n * ```typescript\n * const parsed = parseWebhookSignature(request.headers['x-webhook-signature'])\n * if (parsed) {\n * console.log('Timestamp:', parsed.timestamp)\n * console.log('Signature:', parsed.signature)\n * }\n * ```\n */\nexport const parseWebhookSignature = (\n signatureHeader: string\n): ParsedWebhookSignature | null => {\n const parts = signatureHeader.split(\",\")\n\n let timestamp: number | null = null\n let signature: string | null = null\n\n for (const part of parts) {\n const eqIndex = part.indexOf(\"=\")\n if (eqIndex === -1) continue\n const key = part.slice(0, eqIndex)\n const value = part.slice(eqIndex + 1)\n if (key === \"t\") {\n timestamp = parseInt(value, 10)\n } else if (key === \"v1\") {\n signature = value\n }\n }\n\n if (timestamp === null || signature === null || isNaN(timestamp)) {\n return null\n }\n\n return { timestamp, signature }\n}\n\n/**\n * Verify a webhook signature using HMAC-SHA256.\n *\n * This function verifies that a webhook payload was signed by Locu using your webhook secret.\n * It also checks that the signature timestamp is not too old to prevent replay attacks.\n *\n * @param secret - Your webhook secret (starts with `whsec_`)\n * @param signatureHeader - The X-Webhook-Signature header value\n * @param body - The raw request body as a string\n * @param options - Optional verification settings\n * @returns Object with `valid: true` if valid, or `valid: false` with an error message\n *\n * @example\n * ```typescript\n * import { verifyWebhookSignature } from '@locu/api-client'\n *\n * app.post('/webhooks/locu', (req, res) => {\n * const result = verifyWebhookSignature(\n * process.env.LOCU_WEBHOOK_SECRET,\n * req.headers['x-webhook-signature'],\n * req.body, // raw body string\n * { maxAge: 300 } // 5 minutes\n * )\n *\n * if (!result.valid) {\n * return res.status(401).json({ error: result.error })\n * }\n *\n * // Process the webhook\n * const payload = JSON.parse(req.body)\n * console.log('Received event:', payload.event)\n * })\n * ```\n */\nexport const verifyWebhookSignature = (\n secret: string,\n signatureHeader: string,\n body: string,\n options?: VerifyWebhookOptions\n): WebhookSignatureResult => {\n const parsed = parseWebhookSignature(signatureHeader)\n\n if (!parsed) {\n return { valid: false, error: \"Invalid signature format\" }\n }\n\n const { timestamp, signature } = parsed\n\n // Check timestamp age if maxAge is specified\n if (options?.maxAge !== undefined) {\n const now = Math.floor(Date.now() / 1000)\n const age = now - timestamp\n\n if (age > options.maxAge) {\n return { valid: false, error: \"Signature timestamp too old\" }\n }\n\n if (age < -60) {\n // Allow 1 minute clock skew into the future\n return { valid: false, error: \"Signature timestamp in the future\" }\n }\n }\n\n // Compute expected signature\n const signaturePayload = `${timestamp}.${body}`\n const expectedSignature = createHmac(\"sha256\", secret)\n .update(signaturePayload)\n .digest(\"hex\")\n\n // Use timing-safe comparison to prevent timing attacks\n const signatureBuffer = Buffer.from(signature, \"hex\")\n const expectedBuffer = Buffer.from(expectedSignature, \"hex\")\n\n if (signatureBuffer.length !== expectedBuffer.length) {\n return { valid: false, error: \"Invalid signature\" }\n }\n\n const isValid = timingSafeEqual(signatureBuffer, expectedBuffer)\n\n if (!isValid) {\n return { valid: false, error: \"Invalid signature\" }\n }\n\n return { valid: true }\n}\n\n/**\n * Parse a webhook payload from a JSON string.\n *\n * @param body - The raw request body as a JSON string\n * @returns The parsed webhook payload\n *\n * @example\n * ```typescript\n * import { parseWebhookPayload, TaskWebhookPayload } from '@locu/api-client'\n *\n * const payload = parseWebhookPayload<TaskWebhookPayload>(req.body)\n * console.log('Event:', payload.event) // e.g., \"task.created\"\n * console.log('Task name:', payload.data.name)\n * ```\n */\nexport const parseWebhookPayload = <T = unknown>(\n body: string\n): WebhookPayload<T> => {\n return JSON.parse(body) as WebhookPayload<T>\n}\n\n/**\n * Generate a webhook signature for testing purposes.\n *\n * This is useful for testing your webhook handlers locally.\n *\n * @param secret - Your webhook secret\n * @param timestamp - Unix timestamp in seconds\n * @param body - The request body as a string\n * @returns The signature header value in format `t=<timestamp>,v1=<signature>`\n *\n * @example\n * ```typescript\n * import { generateWebhookSignature } from '@locu/api-client'\n *\n * const body = JSON.stringify({ event: 'task.created', timestamp: '...', data: {...} })\n * const signature = generateWebhookSignature('whsec_...', Math.floor(Date.now() / 1000), body)\n * // Use signature for testing your webhook handler\n * ```\n */\nexport const generateWebhookSignature = (\n secret: string,\n timestamp: number,\n body: string\n): string => {\n const signaturePayload = `${timestamp}.${body}`\n const signature = createHmac(\"sha256\", secret)\n .update(signaturePayload)\n .digest(\"hex\")\n return `t=${timestamp},v1=${signature}`\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loculabs/api-client",
3
- "version": "1.4.0",
3
+ "version": "2.0.0",
4
4
  "description": "TypeScript client for the Locu REST API",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",