@devwithbobby/loops 0.1.13 → 0.1.14

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.
@@ -0,0 +1,7 @@
1
+ export declare const LOOPS_API_BASE_URL = "https://app.loops.so/api/v1";
2
+ export declare const sanitizeLoopsError: (status: number, _errorText: string) => Error;
3
+ export type LoopsRequestInit = Omit<RequestInit, "body"> & {
4
+ json?: unknown;
5
+ };
6
+ export declare const loopsFetch: (apiKey: string, path: string, init?: LoopsRequestInit) => Promise<Response>;
7
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/component/helpers.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB,gCAAgC,CAAC;AAEhE,eAAO,MAAM,kBAAkB,GAC9B,QAAQ,MAAM,EACd,YAAY,MAAM,KAChB,KAcF,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,GAAG;IAC1D,IAAI,CAAC,EAAE,OAAO,CAAC;CACf,CAAC;AAEF,eAAO,MAAM,UAAU,GACtB,QAAQ,MAAM,EACd,MAAM,MAAM,EACZ,OAAM,gBAAqB,sBAe3B,CAAC"}
@@ -0,0 +1,30 @@
1
+ export const LOOPS_API_BASE_URL = "https://app.loops.so/api/v1";
2
+ export const sanitizeLoopsError = (status, _errorText) => {
3
+ if (status === 401 || status === 403) {
4
+ return new Error("Authentication failed. Please check your API key.");
5
+ }
6
+ if (status === 404) {
7
+ return new Error("Resource not found.");
8
+ }
9
+ if (status === 429) {
10
+ return new Error("Rate limit exceeded. Please try again later.");
11
+ }
12
+ if (status >= 500) {
13
+ return new Error("Loops service error. Please try again later.");
14
+ }
15
+ return new Error(`Loops API error (${status}). Please try again.`);
16
+ };
17
+ export const loopsFetch = async (apiKey, path, init = {}) => {
18
+ const { json, ...rest } = init;
19
+ const headers = new Headers(rest.headers ?? {});
20
+ headers.set("Authorization", `Bearer ${apiKey}`);
21
+ if (json !== undefined && !headers.has("Content-Type")) {
22
+ headers.set("Content-Type", "application/json");
23
+ }
24
+ return fetch(`${LOOPS_API_BASE_URL}${path}`, {
25
+ ...rest,
26
+ headers,
27
+ // @ts-expect-error RequestInit in this build doesn't declare body
28
+ body: json !== undefined ? JSON.stringify(json) : rest.body,
29
+ });
30
+ };
@@ -0,0 +1,3 @@
1
+ declare const http: import("convex/server").HttpRouter;
2
+ export default http;
3
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/component/http.ts"],"names":[],"mappings":"AAaA,QAAA,MAAM,IAAI,oCAAe,CAAC;AAkS1B,eAAe,IAAI,CAAC"}
@@ -0,0 +1,268 @@
1
+ import { httpRouter } from "convex/server";
2
+ import { internalLib, } from "../types";
3
+ import { httpAction } from "./_generated/server";
4
+ const http = httpRouter();
5
+ const allowedOrigin = process.env.LOOPS_HTTP_ALLOWED_ORIGIN ?? process.env.CLIENT_ORIGIN ?? "*";
6
+ const buildCorsHeaders = (extra) => {
7
+ const headers = new Headers(extra ?? {});
8
+ headers.set("Access-Control-Allow-Origin", allowedOrigin);
9
+ headers.set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
10
+ headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
11
+ headers.set("Access-Control-Max-Age", "86400");
12
+ headers.set("Vary", "Origin");
13
+ return headers;
14
+ };
15
+ const jsonResponse = (data, init) => {
16
+ const headers = buildCorsHeaders(init?.headers ?? undefined);
17
+ headers.set("Content-Type", "application/json");
18
+ return new Response(JSON.stringify(data), { ...init, headers });
19
+ };
20
+ const emptyResponse = (init) => {
21
+ const headers = buildCorsHeaders(init?.headers ?? undefined);
22
+ return new Response(null, { ...init, headers });
23
+ };
24
+ const readJsonBody = async (request) => {
25
+ try {
26
+ return (await request.json());
27
+ }
28
+ catch (_error) {
29
+ throw new Error("Invalid JSON body");
30
+ }
31
+ };
32
+ const booleanFromQuery = (value) => {
33
+ if (value === null) {
34
+ return undefined;
35
+ }
36
+ if (value === "true") {
37
+ return true;
38
+ }
39
+ if (value === "false") {
40
+ return false;
41
+ }
42
+ return undefined;
43
+ };
44
+ const numberFromQuery = (value, fallback) => {
45
+ if (!value) {
46
+ return fallback;
47
+ }
48
+ const parsed = Number.parseInt(value, 10);
49
+ return Number.isNaN(parsed) ? fallback : parsed;
50
+ };
51
+ const requireLoopsApiKey = () => {
52
+ const apiKey = process.env.LOOPS_API_KEY;
53
+ if (!apiKey) {
54
+ throw new Error("LOOPS_API_KEY environment variable must be set to use the HTTP API.");
55
+ }
56
+ return apiKey;
57
+ };
58
+ const respondError = (error) => {
59
+ console.error("[loops:http]", error);
60
+ const message = error instanceof Error ? error.message : "Unexpected error";
61
+ const status = error instanceof Error &&
62
+ error.message.includes("LOOPS_API_KEY environment variable")
63
+ ? 500
64
+ : 400;
65
+ return jsonResponse({ error: message }, { status });
66
+ };
67
+ http.route({
68
+ pathPrefix: "/loops",
69
+ method: "OPTIONS",
70
+ handler: httpAction(async (_ctx, request) => {
71
+ const headers = buildCorsHeaders();
72
+ const requestedHeaders = request.headers.get("Access-Control-Request-Headers");
73
+ if (requestedHeaders) {
74
+ headers.set("Access-Control-Allow-Headers", requestedHeaders);
75
+ }
76
+ const requestedMethod = request.headers.get("Access-Control-Request-Method");
77
+ if (requestedMethod) {
78
+ headers.set("Access-Control-Allow-Methods", `${requestedMethod},OPTIONS`);
79
+ }
80
+ return new Response(null, { status: 204, headers });
81
+ }),
82
+ });
83
+ http.route({
84
+ path: "/loops/contacts",
85
+ method: "POST",
86
+ handler: httpAction(async (ctx, request) => {
87
+ try {
88
+ const contact = await readJsonBody(request);
89
+ const data = await ctx.runAction(internalLib.addContact, {
90
+ apiKey: requireLoopsApiKey(),
91
+ contact,
92
+ });
93
+ return jsonResponse(data, { status: 201 });
94
+ }
95
+ catch (error) {
96
+ return respondError(error);
97
+ }
98
+ }),
99
+ });
100
+ http.route({
101
+ path: "/loops/contacts",
102
+ method: "PUT",
103
+ handler: httpAction(async (ctx, request) => {
104
+ try {
105
+ const payload = await readJsonBody(request);
106
+ if (!payload.email) {
107
+ throw new Error("email is required");
108
+ }
109
+ const data = await ctx.runAction(internalLib.updateContact, {
110
+ apiKey: requireLoopsApiKey(),
111
+ email: payload.email,
112
+ dataVariables: payload.dataVariables,
113
+ firstName: payload.firstName,
114
+ lastName: payload.lastName,
115
+ userId: payload.userId,
116
+ source: payload.source,
117
+ subscribed: payload.subscribed,
118
+ userGroup: payload.userGroup,
119
+ });
120
+ return jsonResponse(data);
121
+ }
122
+ catch (error) {
123
+ return respondError(error);
124
+ }
125
+ }),
126
+ });
127
+ http.route({
128
+ path: "/loops/contacts",
129
+ method: "GET",
130
+ handler: httpAction(async (ctx, request) => {
131
+ try {
132
+ const url = new URL(request.url);
133
+ const email = url.searchParams.get("email");
134
+ if (email) {
135
+ const data = await ctx.runAction(internalLib.findContact, {
136
+ apiKey: requireLoopsApiKey(),
137
+ email,
138
+ });
139
+ return jsonResponse(data);
140
+ }
141
+ const data = await ctx.runQuery(internalLib.listContacts, {
142
+ userGroup: url.searchParams.get("userGroup") ?? undefined,
143
+ source: url.searchParams.get("source") ?? undefined,
144
+ subscribed: booleanFromQuery(url.searchParams.get("subscribed")),
145
+ limit: numberFromQuery(url.searchParams.get("limit"), 100),
146
+ offset: numberFromQuery(url.searchParams.get("offset"), 0),
147
+ });
148
+ return jsonResponse(data);
149
+ }
150
+ catch (error) {
151
+ return respondError(error);
152
+ }
153
+ }),
154
+ });
155
+ http.route({
156
+ path: "/loops/contacts",
157
+ method: "DELETE",
158
+ handler: httpAction(async (ctx, request) => {
159
+ try {
160
+ const payload = await readJsonBody(request);
161
+ if (!payload.email) {
162
+ throw new Error("email is required");
163
+ }
164
+ await ctx.runAction(internalLib.deleteContact, {
165
+ apiKey: requireLoopsApiKey(),
166
+ email: payload.email,
167
+ });
168
+ return emptyResponse({ status: 204 });
169
+ }
170
+ catch (error) {
171
+ return respondError(error);
172
+ }
173
+ }),
174
+ });
175
+ http.route({
176
+ path: "/loops/transactional",
177
+ method: "POST",
178
+ handler: httpAction(async (ctx, request) => {
179
+ try {
180
+ const payload = await readJsonBody(request);
181
+ if (!payload.transactionalId) {
182
+ throw new Error("transactionalId is required");
183
+ }
184
+ if (!payload.email) {
185
+ throw new Error("email is required");
186
+ }
187
+ const data = await ctx.runAction(internalLib.sendTransactional, {
188
+ apiKey: requireLoopsApiKey(),
189
+ transactionalId: payload.transactionalId,
190
+ email: payload.email,
191
+ dataVariables: payload.dataVariables,
192
+ });
193
+ return jsonResponse(data);
194
+ }
195
+ catch (error) {
196
+ return respondError(error);
197
+ }
198
+ }),
199
+ });
200
+ http.route({
201
+ path: "/loops/events",
202
+ method: "POST",
203
+ handler: httpAction(async (ctx, request) => {
204
+ try {
205
+ const payload = await readJsonBody(request);
206
+ if (!payload.email) {
207
+ throw new Error("email is required");
208
+ }
209
+ if (!payload.eventName) {
210
+ throw new Error("eventName is required");
211
+ }
212
+ const data = await ctx.runAction(internalLib.sendEvent, {
213
+ apiKey: requireLoopsApiKey(),
214
+ email: payload.email,
215
+ eventName: payload.eventName,
216
+ eventProperties: payload.eventProperties,
217
+ });
218
+ return jsonResponse(data);
219
+ }
220
+ catch (error) {
221
+ return respondError(error);
222
+ }
223
+ }),
224
+ });
225
+ http.route({
226
+ path: "/loops/trigger",
227
+ method: "POST",
228
+ handler: httpAction(async (ctx, request) => {
229
+ try {
230
+ const payload = await readJsonBody(request);
231
+ if (!payload.loopId) {
232
+ throw new Error("loopId is required");
233
+ }
234
+ if (!payload.email) {
235
+ throw new Error("email is required");
236
+ }
237
+ const data = await ctx.runAction(internalLib.triggerLoop, {
238
+ apiKey: requireLoopsApiKey(),
239
+ loopId: payload.loopId,
240
+ email: payload.email,
241
+ dataVariables: payload.dataVariables,
242
+ eventName: payload.eventName,
243
+ });
244
+ return jsonResponse(data);
245
+ }
246
+ catch (error) {
247
+ return respondError(error);
248
+ }
249
+ }),
250
+ });
251
+ http.route({
252
+ path: "/loops/stats",
253
+ method: "GET",
254
+ handler: httpAction(async (ctx, request) => {
255
+ try {
256
+ const url = new URL(request.url);
257
+ const timeWindowMs = numberFromQuery(url.searchParams.get("timeWindowMs"), 86400000);
258
+ const data = await ctx.runQuery(internalLib.getEmailStats, {
259
+ timeWindowMs,
260
+ });
261
+ return jsonResponse(data);
262
+ }
263
+ catch (error) {
264
+ return respondError(error);
265
+ }
266
+ }),
267
+ });
268
+ export default http;
@@ -1 +1 @@
1
- {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../src/component/lib.ts"],"names":[],"mappings":"AA2BA;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;;;;;iBA6CvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;iBAexB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;iBAiC5B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,aAAa;;;;mBAwCxB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;GAmFvB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;GA4HrB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;GAoDxB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;GAqD5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,SAAS;;;;;;;GAgCpB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;GA8BxB,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,WAAW;;;;;;;;;GA4DtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;GAgEtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;GAiE9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;;GA+B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;;GA+B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;KA8C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;;;KA4C1B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;GAkDxB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;KAsGlC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;GA+ClC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;GA6C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;GA8B/B,CAAC"}
1
+ {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../src/component/lib.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;;;;;iBA6CvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;iBAexB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;iBAkC5B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,aAAa;;;;mBAwCxB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;GAmFvB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;GA+GrB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;GAgDxB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;GAiD5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,SAAS;;;;;;;GA4BpB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;GA0BxB,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,WAAW;;;;;;;;;GA4DtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;GA4DtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;GA8D9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;;GA2B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;;GA2B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;KA8C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;;;KA4C1B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;GAkDxB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;KAsGlC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;GA+ClC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;GA6C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;GA8B/B,CAAC"}