@loculabs/api-client 1.0.0 → 1.2.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/CHANGELOG.md +14 -0
- package/dist/index.d.mts +34 -21
- package/dist/index.d.ts +34 -21
- package/dist/index.js +18 -13
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +18 -13
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.2.0](https://github.com/loculabs/api-client/compare/v1.1.0...v1.2.0) (2026-01-27)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **types:** accept boolean values for string boolean params ([3a62057](https://github.com/loculabs/api-client/commit/3a62057077c46176bf81799474151ccaa7fa708b))
|
|
7
|
+
|
|
8
|
+
# [1.1.0](https://github.com/loculabs/api-client/compare/v1.0.0...v1.1.0) (2026-01-21)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add /me endpoint and refactor client generator ([20dde23](https://github.com/loculabs/api-client/commit/20dde231a71e45f366f3ed1a2501dfe9c6432ce9))
|
|
14
|
+
|
|
1
15
|
# 1.0.0 (2025-12-31)
|
|
2
16
|
|
|
3
17
|
|
package/dist/index.d.mts
CHANGED
|
@@ -176,6 +176,17 @@ interface components {
|
|
|
176
176
|
type: "jira";
|
|
177
177
|
};
|
|
178
178
|
Task: components["schemas"]["LocuTask"] | components["schemas"]["LinearTask"] | components["schemas"]["JiraTask"];
|
|
179
|
+
MeResponse: {
|
|
180
|
+
/** @description Email of the authenticated user */
|
|
181
|
+
email: string;
|
|
182
|
+
/** @description Name of the workspace */
|
|
183
|
+
workspaceName: string;
|
|
184
|
+
};
|
|
185
|
+
ErrorResponse: {
|
|
186
|
+
error: string;
|
|
187
|
+
message: string;
|
|
188
|
+
code?: string;
|
|
189
|
+
};
|
|
179
190
|
Note: {
|
|
180
191
|
id: string;
|
|
181
192
|
text: string;
|
|
@@ -258,11 +269,6 @@ interface components {
|
|
|
258
269
|
nextCursor: string | null;
|
|
259
270
|
hasMore: boolean;
|
|
260
271
|
};
|
|
261
|
-
ErrorResponse: {
|
|
262
|
-
error: string;
|
|
263
|
-
message: string;
|
|
264
|
-
code?: string;
|
|
265
|
-
};
|
|
266
272
|
CreateNoteRequest: {
|
|
267
273
|
/**
|
|
268
274
|
* Format: uuid
|
|
@@ -791,6 +797,7 @@ type WebhookDeliveryListResponse = components["schemas"]["WebhookDeliveryListRes
|
|
|
791
797
|
type RotateSecretResponse = components["schemas"]["RotateSecretResponse"];
|
|
792
798
|
type DeleteWebhookResponse = components["schemas"]["DeleteWebhookResponse"];
|
|
793
799
|
type ErrorResponse = components["schemas"]["ErrorResponse"];
|
|
800
|
+
type MeResponse = components["schemas"]["MeResponse"];
|
|
794
801
|
type ApiError = ErrorResponse;
|
|
795
802
|
type WebhookPayload<T = unknown> = {
|
|
796
803
|
/** Event type, e.g. "task.created", "project.updated" */
|
|
@@ -809,8 +816,10 @@ type PaginationParams = {
|
|
|
809
816
|
limit?: number;
|
|
810
817
|
cursor?: string;
|
|
811
818
|
};
|
|
819
|
+
/** Accepts boolean or string representation for query params */
|
|
820
|
+
type BooleanParam = boolean | "true" | "false";
|
|
812
821
|
type TaskListParams = PaginationParams & {
|
|
813
|
-
done?:
|
|
822
|
+
done?: BooleanParam;
|
|
814
823
|
projectId?: string;
|
|
815
824
|
parentId?: string;
|
|
816
825
|
section?: "today" | "sooner" | "later";
|
|
@@ -823,7 +832,7 @@ type TaskSectionsParams = {
|
|
|
823
832
|
includeHtml?: boolean | null;
|
|
824
833
|
};
|
|
825
834
|
type SubtaskListParams = PaginationParams & {
|
|
826
|
-
done?:
|
|
835
|
+
done?: BooleanParam;
|
|
827
836
|
includeHtml?: boolean | null;
|
|
828
837
|
};
|
|
829
838
|
type ProjectListParams = PaginationParams & {
|
|
@@ -840,7 +849,7 @@ type SessionListParams = PaginationParams & {
|
|
|
840
849
|
includeActivities?: boolean | null;
|
|
841
850
|
};
|
|
842
851
|
type WebhookListParams = PaginationParams & {
|
|
843
|
-
isActive?:
|
|
852
|
+
isActive?: BooleanParam;
|
|
844
853
|
};
|
|
845
854
|
|
|
846
855
|
type LocuClientConfig = {
|
|
@@ -857,6 +866,22 @@ declare class LocuApiError extends Error {
|
|
|
857
866
|
constructor(message: string, status: number, code?: string);
|
|
858
867
|
}
|
|
859
868
|
declare const createLocuClient: (config: LocuClientConfig) => {
|
|
869
|
+
me: {
|
|
870
|
+
/** Get current me */
|
|
871
|
+
get: () => Promise<MeResponse>;
|
|
872
|
+
};
|
|
873
|
+
timer: {
|
|
874
|
+
/** Get current timer */
|
|
875
|
+
get: () => Promise<TimerState>;
|
|
876
|
+
/** Start a new timer */
|
|
877
|
+
start: (data?: StartTimerRequest) => Promise<TimerState>;
|
|
878
|
+
/** Pause the running timer */
|
|
879
|
+
pause: () => Promise<TimerState>;
|
|
880
|
+
/** Resume a paused timer */
|
|
881
|
+
continue: () => Promise<TimerState>;
|
|
882
|
+
/** Stop timer and save sessions */
|
|
883
|
+
stop: () => Promise<StopTimerResponse>;
|
|
884
|
+
};
|
|
860
885
|
tasks: {
|
|
861
886
|
/** List all tasks */
|
|
862
887
|
list: (params?: TaskListParams) => Promise<PaginatedResponse<Task>>;
|
|
@@ -953,18 +978,6 @@ declare const createLocuClient: (config: LocuClientConfig) => {
|
|
|
953
978
|
/** List deliveries for a webhook */
|
|
954
979
|
deliveries: (id: string, params?: PaginationParams) => Promise<PaginatedResponse<WebhookDelivery>>;
|
|
955
980
|
};
|
|
956
|
-
timer: {
|
|
957
|
-
/** Get current timer state */
|
|
958
|
-
get: () => Promise<TimerState>;
|
|
959
|
-
/** Start a new timer */
|
|
960
|
-
start: (data?: StartTimerRequest) => Promise<TimerState>;
|
|
961
|
-
/** Pause the running timer */
|
|
962
|
-
pause: () => Promise<TimerState>;
|
|
963
|
-
/** Resume a paused timer */
|
|
964
|
-
continue: () => Promise<TimerState>;
|
|
965
|
-
/** Stop timer and save sessions */
|
|
966
|
-
stop: () => Promise<StopTimerResponse>;
|
|
967
|
-
};
|
|
968
981
|
};
|
|
969
982
|
type LocuClient = ReturnType<typeof createLocuClient>;
|
|
970
983
|
|
|
@@ -1072,4 +1085,4 @@ declare const parseWebhookPayload: <T = unknown>(body: string) => WebhookPayload
|
|
|
1072
1085
|
*/
|
|
1073
1086
|
declare const generateWebhookSignature: (secret: string, timestamp: number, body: string) => string;
|
|
1074
1087
|
|
|
1075
|
-
export { type ActivityListResponse, type ApiError, type CreateActivityRequest, type CreateNoteRequest, type CreateProjectRequest, type CreateSessionRequest, type CreateTaskRequest, type CreateWebhookRequest, type DeleteActivityResponse, type DeleteNoteResponse, type DeleteProjectResponse, type DeleteSessionResponse, type DeleteTaskResponse, type DeleteWebhookResponse, type ErrorResponse, type JiraTask, type LinearTask, LocuApiError, type LocuClient, type LocuClientConfig, type LocuTask, type Note, type NoteListParams, type NoteListResponse, type PaginatedResponse, type PaginationParams, type ParsedWebhookSignature, type Project, type ProjectDescription, type ProjectListParams, type ProjectListResponse, type RotateSecretResponse, type Session, type SessionActivity, type SessionListParams, type SessionListResponse, type SessionWithActivities, type StartTimerRequest, type StopTimerResponse, type StopTimerSession, type SubtaskListParams, type Task, type TaskBySection, type TaskDescription, type TaskListParams, type TaskListResponse, type TaskSectionsParams, type TaskSectionsResponse, type TimerState, type UpdateActivityRequest, type UpdateNoteRequest, type UpdateProjectRequest, type UpdateSessionRequest, type UpdateTaskRequest, type UpdateWebhookRequest, type VerifyWebhookOptions, type Webhook, type WebhookDelivery, type WebhookDeliveryListResponse, type WebhookListParams, type WebhookListResponse, type WebhookPayload, type WebhookSignatureResult, type WebhookWithSecret, createLocuClient, generateWebhookSignature, parseWebhookPayload, parseWebhookSignature, verifyWebhookSignature };
|
|
1088
|
+
export { type ActivityListResponse, type ApiError, type BooleanParam, type CreateActivityRequest, type CreateNoteRequest, type CreateProjectRequest, type CreateSessionRequest, type CreateTaskRequest, type CreateWebhookRequest, type DeleteActivityResponse, type DeleteNoteResponse, type DeleteProjectResponse, type DeleteSessionResponse, type DeleteTaskResponse, type DeleteWebhookResponse, type ErrorResponse, type JiraTask, type LinearTask, LocuApiError, type LocuClient, type LocuClientConfig, type LocuTask, type MeResponse, type Note, type NoteListParams, type NoteListResponse, type PaginatedResponse, type PaginationParams, type ParsedWebhookSignature, type Project, type ProjectDescription, type ProjectListParams, type ProjectListResponse, type RotateSecretResponse, type Session, type SessionActivity, type SessionListParams, type SessionListResponse, type SessionWithActivities, type StartTimerRequest, type StopTimerResponse, type StopTimerSession, type SubtaskListParams, type Task, type TaskBySection, type TaskDescription, type TaskListParams, type TaskListResponse, type TaskSectionsParams, type TaskSectionsResponse, type TimerState, type UpdateActivityRequest, type UpdateNoteRequest, type UpdateProjectRequest, type UpdateSessionRequest, type UpdateTaskRequest, type UpdateWebhookRequest, type VerifyWebhookOptions, type Webhook, type WebhookDelivery, type WebhookDeliveryListResponse, type WebhookListParams, type WebhookListResponse, type WebhookPayload, type WebhookSignatureResult, type WebhookWithSecret, createLocuClient, generateWebhookSignature, parseWebhookPayload, parseWebhookSignature, verifyWebhookSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -176,6 +176,17 @@ interface components {
|
|
|
176
176
|
type: "jira";
|
|
177
177
|
};
|
|
178
178
|
Task: components["schemas"]["LocuTask"] | components["schemas"]["LinearTask"] | components["schemas"]["JiraTask"];
|
|
179
|
+
MeResponse: {
|
|
180
|
+
/** @description Email of the authenticated user */
|
|
181
|
+
email: string;
|
|
182
|
+
/** @description Name of the workspace */
|
|
183
|
+
workspaceName: string;
|
|
184
|
+
};
|
|
185
|
+
ErrorResponse: {
|
|
186
|
+
error: string;
|
|
187
|
+
message: string;
|
|
188
|
+
code?: string;
|
|
189
|
+
};
|
|
179
190
|
Note: {
|
|
180
191
|
id: string;
|
|
181
192
|
text: string;
|
|
@@ -258,11 +269,6 @@ interface components {
|
|
|
258
269
|
nextCursor: string | null;
|
|
259
270
|
hasMore: boolean;
|
|
260
271
|
};
|
|
261
|
-
ErrorResponse: {
|
|
262
|
-
error: string;
|
|
263
|
-
message: string;
|
|
264
|
-
code?: string;
|
|
265
|
-
};
|
|
266
272
|
CreateNoteRequest: {
|
|
267
273
|
/**
|
|
268
274
|
* Format: uuid
|
|
@@ -791,6 +797,7 @@ type WebhookDeliveryListResponse = components["schemas"]["WebhookDeliveryListRes
|
|
|
791
797
|
type RotateSecretResponse = components["schemas"]["RotateSecretResponse"];
|
|
792
798
|
type DeleteWebhookResponse = components["schemas"]["DeleteWebhookResponse"];
|
|
793
799
|
type ErrorResponse = components["schemas"]["ErrorResponse"];
|
|
800
|
+
type MeResponse = components["schemas"]["MeResponse"];
|
|
794
801
|
type ApiError = ErrorResponse;
|
|
795
802
|
type WebhookPayload<T = unknown> = {
|
|
796
803
|
/** Event type, e.g. "task.created", "project.updated" */
|
|
@@ -809,8 +816,10 @@ type PaginationParams = {
|
|
|
809
816
|
limit?: number;
|
|
810
817
|
cursor?: string;
|
|
811
818
|
};
|
|
819
|
+
/** Accepts boolean or string representation for query params */
|
|
820
|
+
type BooleanParam = boolean | "true" | "false";
|
|
812
821
|
type TaskListParams = PaginationParams & {
|
|
813
|
-
done?:
|
|
822
|
+
done?: BooleanParam;
|
|
814
823
|
projectId?: string;
|
|
815
824
|
parentId?: string;
|
|
816
825
|
section?: "today" | "sooner" | "later";
|
|
@@ -823,7 +832,7 @@ type TaskSectionsParams = {
|
|
|
823
832
|
includeHtml?: boolean | null;
|
|
824
833
|
};
|
|
825
834
|
type SubtaskListParams = PaginationParams & {
|
|
826
|
-
done?:
|
|
835
|
+
done?: BooleanParam;
|
|
827
836
|
includeHtml?: boolean | null;
|
|
828
837
|
};
|
|
829
838
|
type ProjectListParams = PaginationParams & {
|
|
@@ -840,7 +849,7 @@ type SessionListParams = PaginationParams & {
|
|
|
840
849
|
includeActivities?: boolean | null;
|
|
841
850
|
};
|
|
842
851
|
type WebhookListParams = PaginationParams & {
|
|
843
|
-
isActive?:
|
|
852
|
+
isActive?: BooleanParam;
|
|
844
853
|
};
|
|
845
854
|
|
|
846
855
|
type LocuClientConfig = {
|
|
@@ -857,6 +866,22 @@ declare class LocuApiError extends Error {
|
|
|
857
866
|
constructor(message: string, status: number, code?: string);
|
|
858
867
|
}
|
|
859
868
|
declare const createLocuClient: (config: LocuClientConfig) => {
|
|
869
|
+
me: {
|
|
870
|
+
/** Get current me */
|
|
871
|
+
get: () => Promise<MeResponse>;
|
|
872
|
+
};
|
|
873
|
+
timer: {
|
|
874
|
+
/** Get current timer */
|
|
875
|
+
get: () => Promise<TimerState>;
|
|
876
|
+
/** Start a new timer */
|
|
877
|
+
start: (data?: StartTimerRequest) => Promise<TimerState>;
|
|
878
|
+
/** Pause the running timer */
|
|
879
|
+
pause: () => Promise<TimerState>;
|
|
880
|
+
/** Resume a paused timer */
|
|
881
|
+
continue: () => Promise<TimerState>;
|
|
882
|
+
/** Stop timer and save sessions */
|
|
883
|
+
stop: () => Promise<StopTimerResponse>;
|
|
884
|
+
};
|
|
860
885
|
tasks: {
|
|
861
886
|
/** List all tasks */
|
|
862
887
|
list: (params?: TaskListParams) => Promise<PaginatedResponse<Task>>;
|
|
@@ -953,18 +978,6 @@ declare const createLocuClient: (config: LocuClientConfig) => {
|
|
|
953
978
|
/** List deliveries for a webhook */
|
|
954
979
|
deliveries: (id: string, params?: PaginationParams) => Promise<PaginatedResponse<WebhookDelivery>>;
|
|
955
980
|
};
|
|
956
|
-
timer: {
|
|
957
|
-
/** Get current timer state */
|
|
958
|
-
get: () => Promise<TimerState>;
|
|
959
|
-
/** Start a new timer */
|
|
960
|
-
start: (data?: StartTimerRequest) => Promise<TimerState>;
|
|
961
|
-
/** Pause the running timer */
|
|
962
|
-
pause: () => Promise<TimerState>;
|
|
963
|
-
/** Resume a paused timer */
|
|
964
|
-
continue: () => Promise<TimerState>;
|
|
965
|
-
/** Stop timer and save sessions */
|
|
966
|
-
stop: () => Promise<StopTimerResponse>;
|
|
967
|
-
};
|
|
968
981
|
};
|
|
969
982
|
type LocuClient = ReturnType<typeof createLocuClient>;
|
|
970
983
|
|
|
@@ -1072,4 +1085,4 @@ declare const parseWebhookPayload: <T = unknown>(body: string) => WebhookPayload
|
|
|
1072
1085
|
*/
|
|
1073
1086
|
declare const generateWebhookSignature: (secret: string, timestamp: number, body: string) => string;
|
|
1074
1087
|
|
|
1075
|
-
export { type ActivityListResponse, type ApiError, type CreateActivityRequest, type CreateNoteRequest, type CreateProjectRequest, type CreateSessionRequest, type CreateTaskRequest, type CreateWebhookRequest, type DeleteActivityResponse, type DeleteNoteResponse, type DeleteProjectResponse, type DeleteSessionResponse, type DeleteTaskResponse, type DeleteWebhookResponse, type ErrorResponse, type JiraTask, type LinearTask, LocuApiError, type LocuClient, type LocuClientConfig, type LocuTask, type Note, type NoteListParams, type NoteListResponse, type PaginatedResponse, type PaginationParams, type ParsedWebhookSignature, type Project, type ProjectDescription, type ProjectListParams, type ProjectListResponse, type RotateSecretResponse, type Session, type SessionActivity, type SessionListParams, type SessionListResponse, type SessionWithActivities, type StartTimerRequest, type StopTimerResponse, type StopTimerSession, type SubtaskListParams, type Task, type TaskBySection, type TaskDescription, type TaskListParams, type TaskListResponse, type TaskSectionsParams, type TaskSectionsResponse, type TimerState, type UpdateActivityRequest, type UpdateNoteRequest, type UpdateProjectRequest, type UpdateSessionRequest, type UpdateTaskRequest, type UpdateWebhookRequest, type VerifyWebhookOptions, type Webhook, type WebhookDelivery, type WebhookDeliveryListResponse, type WebhookListParams, type WebhookListResponse, type WebhookPayload, type WebhookSignatureResult, type WebhookWithSecret, createLocuClient, generateWebhookSignature, parseWebhookPayload, parseWebhookSignature, verifyWebhookSignature };
|
|
1088
|
+
export { type ActivityListResponse, type ApiError, type BooleanParam, type CreateActivityRequest, type CreateNoteRequest, type CreateProjectRequest, type CreateSessionRequest, type CreateTaskRequest, type CreateWebhookRequest, type DeleteActivityResponse, type DeleteNoteResponse, type DeleteProjectResponse, type DeleteSessionResponse, type DeleteTaskResponse, type DeleteWebhookResponse, type ErrorResponse, type JiraTask, type LinearTask, LocuApiError, type LocuClient, type LocuClientConfig, type LocuTask, type MeResponse, type Note, type NoteListParams, type NoteListResponse, type PaginatedResponse, type PaginationParams, type ParsedWebhookSignature, type Project, type ProjectDescription, type ProjectListParams, type ProjectListResponse, type RotateSecretResponse, type Session, type SessionActivity, type SessionListParams, type SessionListResponse, type SessionWithActivities, type StartTimerRequest, type StopTimerResponse, type StopTimerSession, type SubtaskListParams, type Task, type TaskBySection, type TaskDescription, type TaskListParams, type TaskListResponse, type TaskSectionsParams, type TaskSectionsResponse, type TimerState, type UpdateActivityRequest, type UpdateNoteRequest, type UpdateProjectRequest, type UpdateSessionRequest, type UpdateTaskRequest, type UpdateWebhookRequest, type VerifyWebhookOptions, type Webhook, type WebhookDelivery, type WebhookDeliveryListResponse, type WebhookListParams, type WebhookListResponse, type WebhookPayload, type WebhookSignatureResult, type WebhookWithSecret, createLocuClient, generateWebhookSignature, parseWebhookPayload, parseWebhookSignature, verifyWebhookSignature };
|
package/dist/index.js
CHANGED
|
@@ -82,6 +82,24 @@ var createLocuClient = (config) => {
|
|
|
82
82
|
return response.json();
|
|
83
83
|
};
|
|
84
84
|
return {
|
|
85
|
+
// ============ Me ============
|
|
86
|
+
me: {
|
|
87
|
+
/** Get current me */
|
|
88
|
+
get: () => request("GET", "/me")
|
|
89
|
+
},
|
|
90
|
+
// ============ Timer ============
|
|
91
|
+
timer: {
|
|
92
|
+
/** Get current timer */
|
|
93
|
+
get: () => request("GET", "/timer"),
|
|
94
|
+
/** Start a new timer */
|
|
95
|
+
start: (data) => request("POST", "/timer/start", data),
|
|
96
|
+
/** Pause the running timer */
|
|
97
|
+
pause: () => request("POST", "/timer/pause"),
|
|
98
|
+
/** Resume a paused timer */
|
|
99
|
+
continue: () => request("POST", "/timer/continue"),
|
|
100
|
+
/** Stop timer and save sessions */
|
|
101
|
+
stop: () => request("POST", "/timer/stop")
|
|
102
|
+
},
|
|
85
103
|
// ============ Tasks ============
|
|
86
104
|
tasks: {
|
|
87
105
|
/** List all tasks */
|
|
@@ -171,19 +189,6 @@ var createLocuClient = (config) => {
|
|
|
171
189
|
rotateSecret: (id) => request("POST", `/webhooks/${id}/rotate-secret`),
|
|
172
190
|
/** List deliveries for a webhook */
|
|
173
191
|
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
192
|
}
|
|
188
193
|
};
|
|
189
194
|
};
|
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\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":[]}
|
|
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 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\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 // 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/dist/index.mjs
CHANGED
|
@@ -51,6 +51,24 @@ var createLocuClient = (config) => {
|
|
|
51
51
|
return response.json();
|
|
52
52
|
};
|
|
53
53
|
return {
|
|
54
|
+
// ============ Me ============
|
|
55
|
+
me: {
|
|
56
|
+
/** Get current me */
|
|
57
|
+
get: () => request("GET", "/me")
|
|
58
|
+
},
|
|
59
|
+
// ============ Timer ============
|
|
60
|
+
timer: {
|
|
61
|
+
/** Get current timer */
|
|
62
|
+
get: () => request("GET", "/timer"),
|
|
63
|
+
/** Start a new timer */
|
|
64
|
+
start: (data) => request("POST", "/timer/start", data),
|
|
65
|
+
/** Pause the running timer */
|
|
66
|
+
pause: () => request("POST", "/timer/pause"),
|
|
67
|
+
/** Resume a paused timer */
|
|
68
|
+
continue: () => request("POST", "/timer/continue"),
|
|
69
|
+
/** Stop timer and save sessions */
|
|
70
|
+
stop: () => request("POST", "/timer/stop")
|
|
71
|
+
},
|
|
54
72
|
// ============ Tasks ============
|
|
55
73
|
tasks: {
|
|
56
74
|
/** List all tasks */
|
|
@@ -140,19 +158,6 @@ var createLocuClient = (config) => {
|
|
|
140
158
|
rotateSecret: (id) => request("POST", `/webhooks/${id}/rotate-secret`),
|
|
141
159
|
/** List deliveries for a webhook */
|
|
142
160
|
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
161
|
}
|
|
157
162
|
};
|
|
158
163
|
};
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +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":[]}
|
|
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 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\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 // 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":[]}
|