@loculabs/api-client 1.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 ADDED
@@ -0,0 +1,259 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ LocuApiError: () => LocuApiError,
24
+ createLocuClient: () => createLocuClient,
25
+ generateWebhookSignature: () => generateWebhookSignature,
26
+ parseWebhookPayload: () => parseWebhookPayload,
27
+ parseWebhookSignature: () => parseWebhookSignature,
28
+ verifyWebhookSignature: () => verifyWebhookSignature
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/client.ts
33
+ var LocuApiError = class extends Error {
34
+ status;
35
+ code;
36
+ constructor(message, status, code) {
37
+ super(message);
38
+ this.name = "LocuApiError";
39
+ this.status = status;
40
+ this.code = code;
41
+ }
42
+ };
43
+ var buildQueryString = (params) => {
44
+ const searchParams = new URLSearchParams();
45
+ for (const [key, value] of Object.entries(params)) {
46
+ if (value !== void 0 && value !== null) {
47
+ searchParams.set(key, String(value));
48
+ }
49
+ }
50
+ const qs = searchParams.toString();
51
+ return qs ? `?${qs}` : "";
52
+ };
53
+ var createLocuClient = (config) => {
54
+ const baseUrl = config.baseUrl || "https://api.locu.app/api/v1";
55
+ const fetchFn = config.fetch || fetch;
56
+ const request = async (method, path, body) => {
57
+ const url = `${baseUrl}${path}`;
58
+ const headers = {
59
+ Authorization: `Bearer ${config.token}`,
60
+ "Content-Type": "application/json"
61
+ };
62
+ const response = await fetchFn(url, {
63
+ method,
64
+ headers,
65
+ body: body ? JSON.stringify(body) : void 0
66
+ });
67
+ if (!response.ok) {
68
+ let errorData = null;
69
+ try {
70
+ errorData = await response.json();
71
+ } catch {
72
+ }
73
+ throw new LocuApiError(
74
+ errorData?.message || `Request failed with status ${response.status}`,
75
+ response.status,
76
+ errorData?.code
77
+ );
78
+ }
79
+ if (response.status === 204) {
80
+ return void 0;
81
+ }
82
+ return response.json();
83
+ };
84
+ return {
85
+ // ============ Tasks ============
86
+ tasks: {
87
+ /** List all tasks */
88
+ list: (params = {}) => request("GET", `/tasks${buildQueryString(params)}`),
89
+ /** Get a single task by ID */
90
+ get: (id) => request("GET", `/tasks/${id}`),
91
+ /** Create a new task */
92
+ create: (data) => request("POST", "/tasks", data),
93
+ /** Update an existing task */
94
+ update: (id, data) => request("PATCH", `/tasks/${id}`, data),
95
+ /** Delete a task */
96
+ delete: (id) => request("DELETE", `/tasks/${id}`),
97
+ /** Get tasks organized by section (today, sooner, later) */
98
+ sections: (params = {}) => request("GET", `/tasks/sections${buildQueryString(params)}`),
99
+ /** List subtasks for a task */
100
+ subtasks: (id, params = {}) => request("GET", `/tasks/${id}/subtasks${buildQueryString(params)}`),
101
+ /** Create a subtask under a parent task */
102
+ createSubtask: (parentId, data) => request("POST", "/tasks", { ...data, parentId })
103
+ },
104
+ // ============ Projects ============
105
+ projects: {
106
+ /** List all projects */
107
+ list: (params = {}) => request("GET", `/projects${buildQueryString(params)}`),
108
+ /** Get a single project by ID */
109
+ get: (id) => request("GET", `/projects/${id}`),
110
+ /** Create a new project */
111
+ create: (data) => request("POST", "/projects", data),
112
+ /** Update an existing project */
113
+ update: (id, data) => request("PATCH", `/projects/${id}`, data),
114
+ /** Delete a project */
115
+ delete: (id) => request("DELETE", `/projects/${id}`)
116
+ },
117
+ // ============ Notes ============
118
+ notes: {
119
+ /** List all notes */
120
+ list: (params = {}) => request("GET", `/notes${buildQueryString(params)}`),
121
+ /** Get a single note by ID */
122
+ get: (id) => request("GET", `/notes/${id}`),
123
+ /** Create a new note */
124
+ create: (data) => request("POST", "/notes", data),
125
+ /** Update an existing note */
126
+ update: (id, data) => request("PATCH", `/notes/${id}`, data),
127
+ /** Delete a note */
128
+ delete: (id) => request("DELETE", `/notes/${id}`)
129
+ },
130
+ // ============ Sessions ============
131
+ sessions: {
132
+ /** List all sessions */
133
+ list: (params = {}) => request("GET", `/sessions${buildQueryString(params)}`),
134
+ /** Get a single session by ID */
135
+ get: (id) => request("GET", `/sessions/${id}`),
136
+ /** Create a new session */
137
+ create: (data) => request("POST", "/sessions", data),
138
+ /** Update an existing session */
139
+ update: (id, data) => request("PATCH", `/sessions/${id}`, data),
140
+ /** Delete a session */
141
+ delete: (id) => request("DELETE", `/sessions/${id}`),
142
+ // Activities
143
+ activities: {
144
+ /** List activities for a session */
145
+ list: (sessionId) => request("GET", `/sessions/${sessionId}/activities`),
146
+ /** Create a new activitie */
147
+ create: (sessionId, data) => request("POST", `/sessions/${sessionId}/activities`, data),
148
+ /** Update an activitie */
149
+ update: (sessionId, activityId, data) => request(
150
+ "PATCH",
151
+ `/sessions/${sessionId}/activities/${activityId}`,
152
+ data
153
+ ),
154
+ /** Delete an activitie */
155
+ delete: (sessionId, activityId) => request("DELETE", `/sessions/${sessionId}/activities/${activityId}`)
156
+ }
157
+ },
158
+ // ============ Webhooks ============
159
+ webhooks: {
160
+ /** List all webhooks */
161
+ list: (params = {}) => request("GET", `/webhooks${buildQueryString(params)}`),
162
+ /** Get a single webhook by ID */
163
+ get: (id) => request("GET", `/webhooks/${id}`),
164
+ /** Create a new webhook */
165
+ create: (data) => request("POST", "/webhooks", data),
166
+ /** Update an existing webhook */
167
+ update: (id, data) => request("PATCH", `/webhooks/${id}`, data),
168
+ /** Delete a webhook */
169
+ delete: (id) => request("DELETE", `/webhooks/${id}`),
170
+ /** Rotate webhook secret */
171
+ rotateSecret: (id) => request("POST", `/webhooks/${id}/rotate-secret`),
172
+ /** List deliveries for a webhook */
173
+ deliveries: (id, params = {}) => request("GET", `/webhooks/${id}/deliveries${buildQueryString(params)}`)
174
+ },
175
+ // ============ Timer ============
176
+ timer: {
177
+ /** Get current timer state */
178
+ get: () => request("GET", "/timer"),
179
+ /** Start a new timer */
180
+ start: (data) => request("POST", "/timer/start", data),
181
+ /** Pause the running timer */
182
+ pause: () => request("POST", "/timer/pause"),
183
+ /** Resume a paused timer */
184
+ continue: () => request("POST", "/timer/continue"),
185
+ /** Stop timer and save sessions */
186
+ stop: () => request("POST", "/timer/stop")
187
+ }
188
+ };
189
+ };
190
+
191
+ // src/webhook.ts
192
+ var import_crypto = require("crypto");
193
+ var parseWebhookSignature = (signatureHeader) => {
194
+ const parts = signatureHeader.split(",");
195
+ let timestamp = null;
196
+ let signature = null;
197
+ for (const part of parts) {
198
+ const eqIndex = part.indexOf("=");
199
+ if (eqIndex === -1) continue;
200
+ const key = part.slice(0, eqIndex);
201
+ const value = part.slice(eqIndex + 1);
202
+ if (key === "t") {
203
+ timestamp = parseInt(value, 10);
204
+ } else if (key === "v1") {
205
+ signature = value;
206
+ }
207
+ }
208
+ if (timestamp === null || signature === null || isNaN(timestamp)) {
209
+ return null;
210
+ }
211
+ return { timestamp, signature };
212
+ };
213
+ var verifyWebhookSignature = (secret, signatureHeader, body, options) => {
214
+ const parsed = parseWebhookSignature(signatureHeader);
215
+ if (!parsed) {
216
+ return { valid: false, error: "Invalid signature format" };
217
+ }
218
+ const { timestamp, signature } = parsed;
219
+ if (options?.maxAge !== void 0) {
220
+ const now = Math.floor(Date.now() / 1e3);
221
+ const age = now - timestamp;
222
+ if (age > options.maxAge) {
223
+ return { valid: false, error: "Signature timestamp too old" };
224
+ }
225
+ if (age < -60) {
226
+ return { valid: false, error: "Signature timestamp in the future" };
227
+ }
228
+ }
229
+ const signaturePayload = `${timestamp}.${body}`;
230
+ const expectedSignature = (0, import_crypto.createHmac)("sha256", secret).update(signaturePayload).digest("hex");
231
+ const signatureBuffer = Buffer.from(signature, "hex");
232
+ const expectedBuffer = Buffer.from(expectedSignature, "hex");
233
+ if (signatureBuffer.length !== expectedBuffer.length) {
234
+ return { valid: false, error: "Invalid signature" };
235
+ }
236
+ const isValid = (0, import_crypto.timingSafeEqual)(signatureBuffer, expectedBuffer);
237
+ if (!isValid) {
238
+ return { valid: false, error: "Invalid signature" };
239
+ }
240
+ return { valid: true };
241
+ };
242
+ var parseWebhookPayload = (body) => {
243
+ return JSON.parse(body);
244
+ };
245
+ var generateWebhookSignature = (secret, timestamp, body) => {
246
+ const signaturePayload = `${timestamp}.${body}`;
247
+ const signature = (0, import_crypto.createHmac)("sha256", secret).update(signaturePayload).digest("hex");
248
+ return `t=${timestamp},v1=${signature}`;
249
+ };
250
+ // Annotate the CommonJS export names for ESM import in node:
251
+ 0 && (module.exports = {
252
+ LocuApiError,
253
+ createLocuClient,
254
+ generateWebhookSignature,
255
+ parseWebhookPayload,
256
+ parseWebhookSignature,
257
+ verifyWebhookSignature
258
+ });
259
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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\n// Types - re-export everything from types module\nexport type * from \"./types\"\n","import type {\n ApiError,\n CreateActivityRequest,\n CreateNoteRequest,\n CreateProjectRequest,\n CreateSessionRequest,\n CreateTaskRequest,\n CreateWebhookRequest,\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 // ============ 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> =>\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: (params: TaskSectionsParams = {}): 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> =>\n request(\"POST\", \"/tasks\", { ...data, parentId }),\n },\n\n // ============ Projects ============\n projects: {\n /** List all projects */\n list: (params: ProjectListParams = {}): Promise<PaginatedResponse<Project>> =>\n request(\"GET\", `/projects${buildQueryString(params)}`),\n\n /** Get a single project by ID */\n get: (id: string): Promise<Project> =>\n 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\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> =>\n 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\n // ============ Sessions ============\n sessions: {\n /** List all sessions */\n list: (params: SessionListParams = {}): 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\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: (params: WebhookListParams = {}): Promise<PaginatedResponse<Webhook>> =>\n request(\"GET\", `/webhooks${buildQueryString(params)}`),\n\n /** Get a single webhook by ID */\n get: (id: string): Promise<Webhook> =>\n 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 // ============ Timer ============\n timer: {\n /** Get current timer state */\n get: (): Promise<TimerState> =>\n 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> =>\n request(\"POST\", \"/timer/pause\"),\n\n /** Resume a paused timer */\n continue: (): Promise<TimerState> =>\n request(\"POST\", \"/timer/continue\"),\n\n /** Stop timer and save sessions */\n stop: (): Promise<StopTimerResponse> =>\n request(\"POST\", \"/timer/stop\"),\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/dist/index.mjs ADDED
@@ -0,0 +1,227 @@
1
+ // src/client.ts
2
+ var LocuApiError = class extends Error {
3
+ status;
4
+ code;
5
+ constructor(message, status, code) {
6
+ super(message);
7
+ this.name = "LocuApiError";
8
+ this.status = status;
9
+ this.code = code;
10
+ }
11
+ };
12
+ var buildQueryString = (params) => {
13
+ const searchParams = new URLSearchParams();
14
+ for (const [key, value] of Object.entries(params)) {
15
+ if (value !== void 0 && value !== null) {
16
+ searchParams.set(key, String(value));
17
+ }
18
+ }
19
+ const qs = searchParams.toString();
20
+ return qs ? `?${qs}` : "";
21
+ };
22
+ var createLocuClient = (config) => {
23
+ const baseUrl = config.baseUrl || "https://api.locu.app/api/v1";
24
+ const fetchFn = config.fetch || fetch;
25
+ const request = async (method, path, body) => {
26
+ const url = `${baseUrl}${path}`;
27
+ const headers = {
28
+ Authorization: `Bearer ${config.token}`,
29
+ "Content-Type": "application/json"
30
+ };
31
+ const response = await fetchFn(url, {
32
+ method,
33
+ headers,
34
+ body: body ? JSON.stringify(body) : void 0
35
+ });
36
+ if (!response.ok) {
37
+ let errorData = null;
38
+ try {
39
+ errorData = await response.json();
40
+ } catch {
41
+ }
42
+ throw new LocuApiError(
43
+ errorData?.message || `Request failed with status ${response.status}`,
44
+ response.status,
45
+ errorData?.code
46
+ );
47
+ }
48
+ if (response.status === 204) {
49
+ return void 0;
50
+ }
51
+ return response.json();
52
+ };
53
+ return {
54
+ // ============ Tasks ============
55
+ tasks: {
56
+ /** List all tasks */
57
+ list: (params = {}) => request("GET", `/tasks${buildQueryString(params)}`),
58
+ /** Get a single task by ID */
59
+ get: (id) => request("GET", `/tasks/${id}`),
60
+ /** Create a new task */
61
+ create: (data) => request("POST", "/tasks", data),
62
+ /** Update an existing task */
63
+ update: (id, data) => request("PATCH", `/tasks/${id}`, data),
64
+ /** Delete a task */
65
+ delete: (id) => request("DELETE", `/tasks/${id}`),
66
+ /** Get tasks organized by section (today, sooner, later) */
67
+ sections: (params = {}) => request("GET", `/tasks/sections${buildQueryString(params)}`),
68
+ /** List subtasks for a task */
69
+ subtasks: (id, params = {}) => request("GET", `/tasks/${id}/subtasks${buildQueryString(params)}`),
70
+ /** Create a subtask under a parent task */
71
+ createSubtask: (parentId, data) => request("POST", "/tasks", { ...data, parentId })
72
+ },
73
+ // ============ Projects ============
74
+ projects: {
75
+ /** List all projects */
76
+ list: (params = {}) => request("GET", `/projects${buildQueryString(params)}`),
77
+ /** Get a single project by ID */
78
+ get: (id) => request("GET", `/projects/${id}`),
79
+ /** Create a new project */
80
+ create: (data) => request("POST", "/projects", data),
81
+ /** Update an existing project */
82
+ update: (id, data) => request("PATCH", `/projects/${id}`, data),
83
+ /** Delete a project */
84
+ delete: (id) => request("DELETE", `/projects/${id}`)
85
+ },
86
+ // ============ Notes ============
87
+ notes: {
88
+ /** List all notes */
89
+ list: (params = {}) => request("GET", `/notes${buildQueryString(params)}`),
90
+ /** Get a single note by ID */
91
+ get: (id) => request("GET", `/notes/${id}`),
92
+ /** Create a new note */
93
+ create: (data) => request("POST", "/notes", data),
94
+ /** Update an existing note */
95
+ update: (id, data) => request("PATCH", `/notes/${id}`, data),
96
+ /** Delete a note */
97
+ delete: (id) => request("DELETE", `/notes/${id}`)
98
+ },
99
+ // ============ Sessions ============
100
+ sessions: {
101
+ /** List all sessions */
102
+ list: (params = {}) => request("GET", `/sessions${buildQueryString(params)}`),
103
+ /** Get a single session by ID */
104
+ get: (id) => request("GET", `/sessions/${id}`),
105
+ /** Create a new session */
106
+ create: (data) => request("POST", "/sessions", data),
107
+ /** Update an existing session */
108
+ update: (id, data) => request("PATCH", `/sessions/${id}`, data),
109
+ /** Delete a session */
110
+ delete: (id) => request("DELETE", `/sessions/${id}`),
111
+ // Activities
112
+ activities: {
113
+ /** List activities for a session */
114
+ list: (sessionId) => request("GET", `/sessions/${sessionId}/activities`),
115
+ /** Create a new activitie */
116
+ create: (sessionId, data) => request("POST", `/sessions/${sessionId}/activities`, data),
117
+ /** Update an activitie */
118
+ update: (sessionId, activityId, data) => request(
119
+ "PATCH",
120
+ `/sessions/${sessionId}/activities/${activityId}`,
121
+ data
122
+ ),
123
+ /** Delete an activitie */
124
+ delete: (sessionId, activityId) => request("DELETE", `/sessions/${sessionId}/activities/${activityId}`)
125
+ }
126
+ },
127
+ // ============ Webhooks ============
128
+ webhooks: {
129
+ /** List all webhooks */
130
+ list: (params = {}) => request("GET", `/webhooks${buildQueryString(params)}`),
131
+ /** Get a single webhook by ID */
132
+ get: (id) => request("GET", `/webhooks/${id}`),
133
+ /** Create a new webhook */
134
+ create: (data) => request("POST", "/webhooks", data),
135
+ /** Update an existing webhook */
136
+ update: (id, data) => request("PATCH", `/webhooks/${id}`, data),
137
+ /** Delete a webhook */
138
+ delete: (id) => request("DELETE", `/webhooks/${id}`),
139
+ /** Rotate webhook secret */
140
+ rotateSecret: (id) => request("POST", `/webhooks/${id}/rotate-secret`),
141
+ /** List deliveries for a webhook */
142
+ deliveries: (id, params = {}) => request("GET", `/webhooks/${id}/deliveries${buildQueryString(params)}`)
143
+ },
144
+ // ============ Timer ============
145
+ timer: {
146
+ /** Get current timer state */
147
+ get: () => request("GET", "/timer"),
148
+ /** Start a new timer */
149
+ start: (data) => request("POST", "/timer/start", data),
150
+ /** Pause the running timer */
151
+ pause: () => request("POST", "/timer/pause"),
152
+ /** Resume a paused timer */
153
+ continue: () => request("POST", "/timer/continue"),
154
+ /** Stop timer and save sessions */
155
+ stop: () => request("POST", "/timer/stop")
156
+ }
157
+ };
158
+ };
159
+
160
+ // src/webhook.ts
161
+ import { createHmac, timingSafeEqual } from "crypto";
162
+ var parseWebhookSignature = (signatureHeader) => {
163
+ const parts = signatureHeader.split(",");
164
+ let timestamp = null;
165
+ let signature = null;
166
+ for (const part of parts) {
167
+ const eqIndex = part.indexOf("=");
168
+ if (eqIndex === -1) continue;
169
+ const key = part.slice(0, eqIndex);
170
+ const value = part.slice(eqIndex + 1);
171
+ if (key === "t") {
172
+ timestamp = parseInt(value, 10);
173
+ } else if (key === "v1") {
174
+ signature = value;
175
+ }
176
+ }
177
+ if (timestamp === null || signature === null || isNaN(timestamp)) {
178
+ return null;
179
+ }
180
+ return { timestamp, signature };
181
+ };
182
+ var verifyWebhookSignature = (secret, signatureHeader, body, options) => {
183
+ const parsed = parseWebhookSignature(signatureHeader);
184
+ if (!parsed) {
185
+ return { valid: false, error: "Invalid signature format" };
186
+ }
187
+ const { timestamp, signature } = parsed;
188
+ if (options?.maxAge !== void 0) {
189
+ const now = Math.floor(Date.now() / 1e3);
190
+ const age = now - timestamp;
191
+ if (age > options.maxAge) {
192
+ return { valid: false, error: "Signature timestamp too old" };
193
+ }
194
+ if (age < -60) {
195
+ return { valid: false, error: "Signature timestamp in the future" };
196
+ }
197
+ }
198
+ const signaturePayload = `${timestamp}.${body}`;
199
+ const expectedSignature = createHmac("sha256", secret).update(signaturePayload).digest("hex");
200
+ const signatureBuffer = Buffer.from(signature, "hex");
201
+ const expectedBuffer = Buffer.from(expectedSignature, "hex");
202
+ if (signatureBuffer.length !== expectedBuffer.length) {
203
+ return { valid: false, error: "Invalid signature" };
204
+ }
205
+ const isValid = timingSafeEqual(signatureBuffer, expectedBuffer);
206
+ if (!isValid) {
207
+ return { valid: false, error: "Invalid signature" };
208
+ }
209
+ return { valid: true };
210
+ };
211
+ var parseWebhookPayload = (body) => {
212
+ return JSON.parse(body);
213
+ };
214
+ var generateWebhookSignature = (secret, timestamp, body) => {
215
+ const signaturePayload = `${timestamp}.${body}`;
216
+ const signature = createHmac("sha256", secret).update(signaturePayload).digest("hex");
217
+ return `t=${timestamp},v1=${signature}`;
218
+ };
219
+ export {
220
+ LocuApiError,
221
+ createLocuClient,
222
+ generateWebhookSignature,
223
+ parseWebhookPayload,
224
+ parseWebhookSignature,
225
+ verifyWebhookSignature
226
+ };
227
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts","../src/webhook.ts"],"sourcesContent":["import type {\n ApiError,\n CreateActivityRequest,\n CreateNoteRequest,\n CreateProjectRequest,\n CreateSessionRequest,\n CreateTaskRequest,\n CreateWebhookRequest,\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 // ============ 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> =>\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: (params: TaskSectionsParams = {}): 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> =>\n request(\"POST\", \"/tasks\", { ...data, parentId }),\n },\n\n // ============ Projects ============\n projects: {\n /** List all projects */\n list: (params: ProjectListParams = {}): Promise<PaginatedResponse<Project>> =>\n request(\"GET\", `/projects${buildQueryString(params)}`),\n\n /** Get a single project by ID */\n get: (id: string): Promise<Project> =>\n 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\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> =>\n 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\n // ============ Sessions ============\n sessions: {\n /** List all sessions */\n list: (params: SessionListParams = {}): 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\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: (params: WebhookListParams = {}): Promise<PaginatedResponse<Webhook>> =>\n request(\"GET\", `/webhooks${buildQueryString(params)}`),\n\n /** Get a single webhook by ID */\n get: (id: string): Promise<Webhook> =>\n 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 // ============ Timer ============\n timer: {\n /** Get current timer state */\n get: (): Promise<TimerState> =>\n 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> =>\n request(\"POST\", \"/timer/pause\"),\n\n /** Resume a paused timer */\n continue: (): Promise<TimerState> =>\n request(\"POST\", \"/timer/continue\"),\n\n /** Stop timer and save sessions */\n stop: (): Promise<StopTimerResponse> =>\n request(\"POST\", \"/timer/stop\"),\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":[]}