@whatalo/plugin-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/express.cjs +87 -0
- package/dist/adapters/express.cjs.map +1 -0
- package/dist/adapters/express.d.cts +17 -0
- package/dist/adapters/express.d.ts +17 -0
- package/dist/adapters/express.mjs +60 -0
- package/dist/adapters/express.mjs.map +1 -0
- package/dist/adapters/hono.cjs +79 -0
- package/dist/adapters/hono.cjs.map +1 -0
- package/dist/adapters/hono.d.cts +15 -0
- package/dist/adapters/hono.d.ts +15 -0
- package/dist/adapters/hono.mjs +52 -0
- package/dist/adapters/hono.mjs.map +1 -0
- package/dist/adapters/nextjs.cjs +79 -0
- package/dist/adapters/nextjs.cjs.map +1 -0
- package/dist/adapters/nextjs.d.cts +7 -0
- package/dist/adapters/nextjs.d.ts +7 -0
- package/dist/adapters/nextjs.mjs +52 -0
- package/dist/adapters/nextjs.mjs.map +1 -0
- package/dist/bridge/index.cjs +290 -0
- package/dist/bridge/index.cjs.map +1 -0
- package/dist/bridge/index.d.cts +236 -0
- package/dist/bridge/index.d.ts +236 -0
- package/dist/bridge/index.mjs +260 -0
- package/dist/bridge/index.mjs.map +1 -0
- package/dist/client/index.cjs +423 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +131 -0
- package/dist/client/index.d.ts +131 -0
- package/dist/client/index.mjs +396 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/index.cjs +843 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +57 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.mjs +801 -0
- package/dist/index.mjs.map +1 -0
- package/dist/manifest/index.cjs +145 -0
- package/dist/manifest/index.cjs.map +1 -0
- package/dist/manifest/index.d.cts +78 -0
- package/dist/manifest/index.d.ts +78 -0
- package/dist/manifest/index.mjs +117 -0
- package/dist/manifest/index.mjs.map +1 -0
- package/dist/types-D2Efg3EG.d.ts +19 -0
- package/dist/types-DZ659i6f.d.ts +68 -0
- package/dist/types-Db_BeRCj.d.cts +19 -0
- package/dist/types-DdqKKyqX.d.cts +68 -0
- package/dist/types-M1eLMz6w.d.cts +279 -0
- package/dist/types-M1eLMz6w.d.ts +279 -0
- package/dist/webhooks/index.cjs +50 -0
- package/dist/webhooks/index.cjs.map +1 -0
- package/dist/webhooks/index.d.cts +18 -0
- package/dist/webhooks/index.d.ts +18 -0
- package/dist/webhooks/index.mjs +23 -0
- package/dist/webhooks/index.mjs.map +1 -0
- package/package.json +94 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
// src/client/errors.ts
|
|
2
|
+
var WhataloAPIError = class extends Error {
|
|
3
|
+
statusCode;
|
|
4
|
+
code;
|
|
5
|
+
requestId;
|
|
6
|
+
constructor(message, statusCode, code, requestId) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "WhataloAPIError";
|
|
9
|
+
this.statusCode = statusCode;
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.requestId = requestId;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var AuthenticationError = class extends WhataloAPIError {
|
|
15
|
+
constructor(message = "Invalid or missing API key", requestId) {
|
|
16
|
+
super(message, 401, "authentication_error", requestId);
|
|
17
|
+
this.name = "AuthenticationError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var AuthorizationError = class extends WhataloAPIError {
|
|
21
|
+
requiredScope;
|
|
22
|
+
constructor(message, requiredScope, requestId) {
|
|
23
|
+
super(message, 403, "authorization_error", requestId);
|
|
24
|
+
this.name = "AuthorizationError";
|
|
25
|
+
this.requiredScope = requiredScope;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var InsufficientScopeError = class extends WhataloAPIError {
|
|
29
|
+
/** The exact scope string missing from the installation's granted_scopes */
|
|
30
|
+
requiredScope;
|
|
31
|
+
/** The scopes currently granted to this installation */
|
|
32
|
+
grantedScopes;
|
|
33
|
+
constructor(message, requiredScope, grantedScopes, requestId) {
|
|
34
|
+
super(message, 403, "insufficient_scope", requestId);
|
|
35
|
+
this.name = "InsufficientScopeError";
|
|
36
|
+
this.requiredScope = requiredScope;
|
|
37
|
+
this.grantedScopes = grantedScopes;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var NotFoundError = class extends WhataloAPIError {
|
|
41
|
+
resourceType;
|
|
42
|
+
resourceId;
|
|
43
|
+
constructor(resourceType, resourceId, requestId) {
|
|
44
|
+
super(
|
|
45
|
+
`${resourceType} '${resourceId}' not found`,
|
|
46
|
+
404,
|
|
47
|
+
"not_found",
|
|
48
|
+
requestId
|
|
49
|
+
);
|
|
50
|
+
this.name = "NotFoundError";
|
|
51
|
+
this.resourceType = resourceType;
|
|
52
|
+
this.resourceId = resourceId;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var ValidationError = class extends WhataloAPIError {
|
|
56
|
+
fieldErrors;
|
|
57
|
+
constructor(fieldErrors, requestId) {
|
|
58
|
+
const message = `Validation failed: ${fieldErrors.map((e) => `${e.field} \u2014 ${e.message}`).join(", ")}`;
|
|
59
|
+
super(message, 422, "validation_error", requestId);
|
|
60
|
+
this.name = "ValidationError";
|
|
61
|
+
this.fieldErrors = fieldErrors;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var RateLimitError = class extends WhataloAPIError {
|
|
65
|
+
/** Seconds until the rate limit resets */
|
|
66
|
+
retryAfter;
|
|
67
|
+
constructor(retryAfter, requestId) {
|
|
68
|
+
super(
|
|
69
|
+
`Rate limit exceeded. Retry after ${retryAfter} seconds.`,
|
|
70
|
+
429,
|
|
71
|
+
"rate_limit_exceeded",
|
|
72
|
+
requestId
|
|
73
|
+
);
|
|
74
|
+
this.name = "RateLimitError";
|
|
75
|
+
this.retryAfter = retryAfter;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var InternalError = class extends WhataloAPIError {
|
|
79
|
+
constructor(message = "An internal error occurred", requestId) {
|
|
80
|
+
super(message, 500, "internal_error", requestId);
|
|
81
|
+
this.name = "InternalError";
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/client/whatalo-client.ts
|
|
86
|
+
var DEFAULT_BASE_URL = "https://api.whatalo.com/v1";
|
|
87
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
88
|
+
var WhataloClient = class {
|
|
89
|
+
apiKey;
|
|
90
|
+
baseUrl;
|
|
91
|
+
timeout;
|
|
92
|
+
maxRetries;
|
|
93
|
+
fetchFn;
|
|
94
|
+
onRequest;
|
|
95
|
+
onResponse;
|
|
96
|
+
/** Rate limit info from the most recent API response */
|
|
97
|
+
rateLimit = { limit: 0, remaining: 0, reset: 0 };
|
|
98
|
+
/** Product resource methods */
|
|
99
|
+
products;
|
|
100
|
+
/** Order resource methods */
|
|
101
|
+
orders;
|
|
102
|
+
/** Customer resource methods */
|
|
103
|
+
customers;
|
|
104
|
+
/** Category resource methods */
|
|
105
|
+
categories;
|
|
106
|
+
/** Discount resource methods */
|
|
107
|
+
discounts;
|
|
108
|
+
/** Store resource methods */
|
|
109
|
+
store;
|
|
110
|
+
/** Inventory resource methods */
|
|
111
|
+
inventory;
|
|
112
|
+
/** Webhook resource methods */
|
|
113
|
+
webhooks;
|
|
114
|
+
constructor(options) {
|
|
115
|
+
this.apiKey = options.apiKey;
|
|
116
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
117
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
118
|
+
this.maxRetries = Math.min(options.retries ?? 0, 3);
|
|
119
|
+
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
120
|
+
this.onRequest = options.onRequest;
|
|
121
|
+
this.onResponse = options.onResponse;
|
|
122
|
+
this.products = new ProductResource(this);
|
|
123
|
+
this.orders = new OrderResource(this);
|
|
124
|
+
this.customers = new CustomerResource(this);
|
|
125
|
+
this.categories = new CategoryResource(this);
|
|
126
|
+
this.discounts = new DiscountResource(this);
|
|
127
|
+
this.store = new StoreResource(this);
|
|
128
|
+
this.inventory = new InventoryResource(this);
|
|
129
|
+
this.webhooks = new WebhookResource(this);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Internal request method shared by all resource namespaces.
|
|
133
|
+
* Handles headers, timeout, error parsing, and rate limit extraction.
|
|
134
|
+
*/
|
|
135
|
+
async request(method, path, options) {
|
|
136
|
+
let url = `${this.baseUrl}${path}`;
|
|
137
|
+
if (options?.params) {
|
|
138
|
+
const searchParams = new URLSearchParams();
|
|
139
|
+
for (const [key, value] of Object.entries(options.params)) {
|
|
140
|
+
if (value !== void 0) {
|
|
141
|
+
searchParams.set(key, String(value));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const qs = searchParams.toString();
|
|
145
|
+
if (qs) url += `?${qs}`;
|
|
146
|
+
}
|
|
147
|
+
const headers = {
|
|
148
|
+
"X-API-Key": this.apiKey,
|
|
149
|
+
"Content-Type": "application/json"
|
|
150
|
+
};
|
|
151
|
+
let lastError = null;
|
|
152
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
153
|
+
const controller = new AbortController();
|
|
154
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
155
|
+
const requestInit = {
|
|
156
|
+
method,
|
|
157
|
+
headers,
|
|
158
|
+
body: options?.body ? JSON.stringify(options.body) : void 0,
|
|
159
|
+
signal: controller.signal
|
|
160
|
+
};
|
|
161
|
+
const startedAt = Date.now();
|
|
162
|
+
this.onRequest?.(url, requestInit);
|
|
163
|
+
try {
|
|
164
|
+
const response = await this.fetchFn(url, requestInit);
|
|
165
|
+
clearTimeout(timeoutId);
|
|
166
|
+
this.onResponse?.(response, Date.now() - startedAt);
|
|
167
|
+
this.rateLimit = {
|
|
168
|
+
limit: Number(response.headers.get("X-RateLimit-Limit") ?? 0),
|
|
169
|
+
remaining: Number(
|
|
170
|
+
response.headers.get("X-RateLimit-Remaining") ?? 0
|
|
171
|
+
),
|
|
172
|
+
reset: Number(response.headers.get("X-RateLimit-Reset") ?? 0)
|
|
173
|
+
};
|
|
174
|
+
const requestId = response.headers.get("X-Request-Id") ?? void 0;
|
|
175
|
+
if (response.ok) {
|
|
176
|
+
return await response.json();
|
|
177
|
+
}
|
|
178
|
+
const errorBody = await response.json().catch(() => ({
|
|
179
|
+
error: { code: "unknown", message: response.statusText }
|
|
180
|
+
}));
|
|
181
|
+
const errData = errorBody.error;
|
|
182
|
+
switch (response.status) {
|
|
183
|
+
case 401:
|
|
184
|
+
throw new AuthenticationError(errData?.message, requestId);
|
|
185
|
+
case 403:
|
|
186
|
+
if (errData?.code === "insufficient_scope") {
|
|
187
|
+
const details = errorBody;
|
|
188
|
+
throw new InsufficientScopeError(
|
|
189
|
+
errData.message ?? "Insufficient scope",
|
|
190
|
+
details.error?.required ?? "",
|
|
191
|
+
details.error?.granted ?? [],
|
|
192
|
+
requestId
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
throw new AuthorizationError(
|
|
196
|
+
errData?.message ?? "Forbidden",
|
|
197
|
+
errData?.code ?? "unknown_scope",
|
|
198
|
+
requestId
|
|
199
|
+
);
|
|
200
|
+
case 404:
|
|
201
|
+
throw new NotFoundError("Resource", path, requestId);
|
|
202
|
+
case 422:
|
|
203
|
+
throw new ValidationError(
|
|
204
|
+
errData?.details ?? [
|
|
205
|
+
{ field: "unknown", message: errData?.message ?? "Validation failed" }
|
|
206
|
+
],
|
|
207
|
+
requestId
|
|
208
|
+
);
|
|
209
|
+
case 429: {
|
|
210
|
+
const retryAfter = Number(
|
|
211
|
+
response.headers.get("Retry-After") ?? 60
|
|
212
|
+
);
|
|
213
|
+
throw new RateLimitError(retryAfter, requestId);
|
|
214
|
+
}
|
|
215
|
+
default:
|
|
216
|
+
if (response.status >= 500 && attempt < this.maxRetries) {
|
|
217
|
+
lastError = new InternalError(errData?.message, requestId);
|
|
218
|
+
await this.sleep(Math.pow(2, attempt) * 1e3);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
throw new WhataloAPIError(
|
|
222
|
+
errData?.message ?? "API error",
|
|
223
|
+
response.status,
|
|
224
|
+
errData?.code ?? "unknown",
|
|
225
|
+
requestId
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
clearTimeout(timeoutId);
|
|
230
|
+
if (err instanceof WhataloAPIError) throw err;
|
|
231
|
+
if (err.name === "AbortError") {
|
|
232
|
+
throw new WhataloAPIError(
|
|
233
|
+
"Request timed out",
|
|
234
|
+
408,
|
|
235
|
+
"timeout"
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
throw lastError ?? new InternalError("Request failed after retries");
|
|
242
|
+
}
|
|
243
|
+
sleep(ms) {
|
|
244
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
var ProductResource = class {
|
|
248
|
+
constructor(client) {
|
|
249
|
+
this.client = client;
|
|
250
|
+
}
|
|
251
|
+
async list(params) {
|
|
252
|
+
return this.client.request("GET", "/products", {
|
|
253
|
+
params
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
async get(id) {
|
|
257
|
+
return this.client.request("GET", `/products/${id}`);
|
|
258
|
+
}
|
|
259
|
+
async create(data) {
|
|
260
|
+
return this.client.request("POST", "/products", { body: data });
|
|
261
|
+
}
|
|
262
|
+
async update(id, data) {
|
|
263
|
+
return this.client.request("PATCH", `/products/${id}`, { body: data });
|
|
264
|
+
}
|
|
265
|
+
async delete(id) {
|
|
266
|
+
return this.client.request("DELETE", `/products/${id}`);
|
|
267
|
+
}
|
|
268
|
+
async count(status) {
|
|
269
|
+
return this.client.request("GET", "/products/count", {
|
|
270
|
+
params: status ? { status } : void 0
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
var OrderResource = class {
|
|
275
|
+
constructor(client) {
|
|
276
|
+
this.client = client;
|
|
277
|
+
}
|
|
278
|
+
async list(params) {
|
|
279
|
+
return this.client.request("GET", "/orders", {
|
|
280
|
+
params
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async get(id) {
|
|
284
|
+
return this.client.request("GET", `/orders/${id}`);
|
|
285
|
+
}
|
|
286
|
+
async updateStatus(id, data) {
|
|
287
|
+
return this.client.request("PATCH", `/orders/${id}`, { body: data });
|
|
288
|
+
}
|
|
289
|
+
async count(status) {
|
|
290
|
+
return this.client.request("GET", "/orders/count", {
|
|
291
|
+
params: status ? { status } : void 0
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
var CustomerResource = class {
|
|
296
|
+
constructor(client) {
|
|
297
|
+
this.client = client;
|
|
298
|
+
}
|
|
299
|
+
async list(params) {
|
|
300
|
+
return this.client.request("GET", "/customers", {
|
|
301
|
+
params
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
async get(id) {
|
|
305
|
+
return this.client.request("GET", `/customers/${id}`);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
var CategoryResource = class {
|
|
309
|
+
constructor(client) {
|
|
310
|
+
this.client = client;
|
|
311
|
+
}
|
|
312
|
+
async list(params) {
|
|
313
|
+
return this.client.request("GET", "/categories", {
|
|
314
|
+
params
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
async get(id) {
|
|
318
|
+
return this.client.request("GET", `/categories/${id}`);
|
|
319
|
+
}
|
|
320
|
+
async create(data) {
|
|
321
|
+
return this.client.request("POST", "/categories", { body: data });
|
|
322
|
+
}
|
|
323
|
+
async update(id, data) {
|
|
324
|
+
return this.client.request("PATCH", `/categories/${id}`, { body: data });
|
|
325
|
+
}
|
|
326
|
+
async delete(id) {
|
|
327
|
+
return this.client.request("DELETE", `/categories/${id}`);
|
|
328
|
+
}
|
|
329
|
+
async count() {
|
|
330
|
+
return this.client.request("GET", "/categories/count");
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
var DiscountResource = class {
|
|
334
|
+
constructor(client) {
|
|
335
|
+
this.client = client;
|
|
336
|
+
}
|
|
337
|
+
async list(params) {
|
|
338
|
+
return this.client.request("GET", "/discounts", {
|
|
339
|
+
params
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
async get(id) {
|
|
343
|
+
return this.client.request("GET", `/discounts/${id}`);
|
|
344
|
+
}
|
|
345
|
+
async create(data) {
|
|
346
|
+
return this.client.request("POST", "/discounts", { body: data });
|
|
347
|
+
}
|
|
348
|
+
async update(id, data) {
|
|
349
|
+
return this.client.request("PATCH", `/discounts/${id}`, { body: data });
|
|
350
|
+
}
|
|
351
|
+
async delete(id) {
|
|
352
|
+
return this.client.request("DELETE", `/discounts/${id}`);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
var StoreResource = class {
|
|
356
|
+
constructor(client) {
|
|
357
|
+
this.client = client;
|
|
358
|
+
}
|
|
359
|
+
async get() {
|
|
360
|
+
return this.client.request("GET", "/store");
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
var InventoryResource = class {
|
|
364
|
+
constructor(client) {
|
|
365
|
+
this.client = client;
|
|
366
|
+
}
|
|
367
|
+
async get(productId) {
|
|
368
|
+
return this.client.request("GET", `/products/${productId}/inventory`);
|
|
369
|
+
}
|
|
370
|
+
async adjust(productId, data) {
|
|
371
|
+
return this.client.request("PATCH", `/products/${productId}/inventory`, {
|
|
372
|
+
body: data
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
var WebhookResource = class {
|
|
377
|
+
constructor(client) {
|
|
378
|
+
this.client = client;
|
|
379
|
+
}
|
|
380
|
+
async list() {
|
|
381
|
+
return this.client.request("GET", "/webhooks");
|
|
382
|
+
}
|
|
383
|
+
async create(data) {
|
|
384
|
+
return this.client.request("POST", "/webhooks", { body: data });
|
|
385
|
+
}
|
|
386
|
+
async update(id, data) {
|
|
387
|
+
return this.client.request("PATCH", `/webhooks/${id}`, { body: data });
|
|
388
|
+
}
|
|
389
|
+
async delete(id) {
|
|
390
|
+
return this.client.request("DELETE", `/webhooks/${id}`);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// src/webhooks/verify.ts
|
|
395
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
396
|
+
function verifyWebhook({
|
|
397
|
+
payload,
|
|
398
|
+
signature,
|
|
399
|
+
secret
|
|
400
|
+
}) {
|
|
401
|
+
if (!payload || !signature || !secret) return false;
|
|
402
|
+
const expected = createHmac("sha256", secret).update(payload, "utf8").digest("hex");
|
|
403
|
+
if (expected.length !== signature.length) return false;
|
|
404
|
+
try {
|
|
405
|
+
return timingSafeEqual(
|
|
406
|
+
Buffer.from(expected, "hex"),
|
|
407
|
+
Buffer.from(signature, "hex")
|
|
408
|
+
);
|
|
409
|
+
} catch {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/manifest/validate.ts
|
|
415
|
+
var ManifestValidationError = class extends Error {
|
|
416
|
+
errors;
|
|
417
|
+
constructor(errors) {
|
|
418
|
+
const message = `Invalid app manifest: ${errors.map((e) => `${e.field}: ${e.message}`).join("; ")}`;
|
|
419
|
+
super(message);
|
|
420
|
+
this.name = "ManifestValidationError";
|
|
421
|
+
this.errors = errors;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
var ID_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
425
|
+
var SEMVER_REGEX = /^\d+\.\d+\.\d+$/;
|
|
426
|
+
var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
427
|
+
var VALID_PERMISSIONS = /* @__PURE__ */ new Set([
|
|
428
|
+
"read:products",
|
|
429
|
+
"write:products",
|
|
430
|
+
"read:orders",
|
|
431
|
+
"write:orders",
|
|
432
|
+
"read:customers",
|
|
433
|
+
"write:customers",
|
|
434
|
+
"read:inventory",
|
|
435
|
+
"write:inventory",
|
|
436
|
+
"read:store",
|
|
437
|
+
"write:store",
|
|
438
|
+
"read:webhooks",
|
|
439
|
+
"write:webhooks",
|
|
440
|
+
"read:discounts",
|
|
441
|
+
"write:discounts",
|
|
442
|
+
"read:analytics"
|
|
443
|
+
]);
|
|
444
|
+
var VALID_CATEGORIES = /* @__PURE__ */ new Set([
|
|
445
|
+
"analytics",
|
|
446
|
+
"marketing",
|
|
447
|
+
"shipping",
|
|
448
|
+
"payments",
|
|
449
|
+
"payment",
|
|
450
|
+
"inventory",
|
|
451
|
+
"communication",
|
|
452
|
+
"productivity",
|
|
453
|
+
"accounting",
|
|
454
|
+
"developer",
|
|
455
|
+
"other"
|
|
456
|
+
]);
|
|
457
|
+
function defineApp(manifest) {
|
|
458
|
+
const errors = [];
|
|
459
|
+
if (!manifest.id || manifest.id.length < 3 || manifest.id.length > 50) {
|
|
460
|
+
errors.push({ field: "id", message: "Must be 3-50 characters" });
|
|
461
|
+
} else if (!ID_REGEX.test(manifest.id)) {
|
|
462
|
+
errors.push({
|
|
463
|
+
field: "id",
|
|
464
|
+
message: "Must be lowercase alphanumeric with hyphens"
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
if (!manifest.name || manifest.name.length < 3 || manifest.name.length > 50) {
|
|
468
|
+
errors.push({ field: "name", message: "Must be 3-50 characters" });
|
|
469
|
+
}
|
|
470
|
+
if (!manifest.description || manifest.description.length < 10 || manifest.description.length > 200) {
|
|
471
|
+
errors.push({
|
|
472
|
+
field: "description",
|
|
473
|
+
message: "Must be 10-200 characters"
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
if (!manifest.version || !SEMVER_REGEX.test(manifest.version)) {
|
|
477
|
+
errors.push({
|
|
478
|
+
field: "version",
|
|
479
|
+
message: "Must be valid semver (e.g., 1.0.0)"
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
if (!manifest.author?.name) {
|
|
483
|
+
errors.push({ field: "author.name", message: "Required" });
|
|
484
|
+
}
|
|
485
|
+
if (!manifest.author?.email || !EMAIL_REGEX.test(manifest.author.email)) {
|
|
486
|
+
errors.push({
|
|
487
|
+
field: "author.email",
|
|
488
|
+
message: "Must be a valid email address"
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
if (!manifest.permissions || manifest.permissions.length === 0) {
|
|
492
|
+
errors.push({
|
|
493
|
+
field: "permissions",
|
|
494
|
+
message: "At least one permission is required"
|
|
495
|
+
});
|
|
496
|
+
} else {
|
|
497
|
+
for (const perm of manifest.permissions) {
|
|
498
|
+
if (!VALID_PERMISSIONS.has(perm)) {
|
|
499
|
+
errors.push({
|
|
500
|
+
field: "permissions",
|
|
501
|
+
message: `Invalid permission: ${perm}`
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (!manifest.appUrl) {
|
|
507
|
+
errors.push({ field: "appUrl", message: "Required" });
|
|
508
|
+
}
|
|
509
|
+
if (manifest.pricing !== "free" && manifest.pricing !== "paid") {
|
|
510
|
+
errors.push({
|
|
511
|
+
field: "pricing",
|
|
512
|
+
message: 'Must be "free" or "paid"'
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
if (!VALID_CATEGORIES.has(manifest.category)) {
|
|
516
|
+
errors.push({
|
|
517
|
+
field: "category",
|
|
518
|
+
message: `Invalid category: ${manifest.category}`
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
if (errors.length > 0) {
|
|
522
|
+
throw new ManifestValidationError(errors);
|
|
523
|
+
}
|
|
524
|
+
return manifest;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// src/bridge/use-whatalo-context.ts
|
|
528
|
+
import { useState, useEffect as useEffect2, useCallback as useCallback2 } from "react";
|
|
529
|
+
|
|
530
|
+
// src/bridge/use-whatalo-action.ts
|
|
531
|
+
import { useCallback, useRef, useEffect } from "react";
|
|
532
|
+
|
|
533
|
+
// src/bridge/types.ts
|
|
534
|
+
var BILLING_OPERATION = {
|
|
535
|
+
GET_PLANS: "getPlans",
|
|
536
|
+
GET_SUBSCRIPTION: "getSubscription",
|
|
537
|
+
REQUEST_SUBSCRIPTION: "requestSubscription",
|
|
538
|
+
CANCEL_SUBSCRIPTION: "cancelSubscription",
|
|
539
|
+
REACTIVATE_SUBSCRIPTION: "reactivateSubscription",
|
|
540
|
+
SWITCH_PLAN: "switchPlan",
|
|
541
|
+
CREATE_USAGE_RECORD: "createUsageRecord"
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// src/bridge/use-whatalo-action.ts
|
|
545
|
+
var ACK_TIMEOUT_MS = 5e3;
|
|
546
|
+
var parentOrigin = null;
|
|
547
|
+
var originResolvers = [];
|
|
548
|
+
function waitForParentOrigin() {
|
|
549
|
+
if (parentOrigin !== null) {
|
|
550
|
+
return Promise.resolve(parentOrigin);
|
|
551
|
+
}
|
|
552
|
+
return new Promise((resolve) => {
|
|
553
|
+
originResolvers.push(resolve);
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
var initListenerAttached = false;
|
|
557
|
+
function attachInitListener() {
|
|
558
|
+
if (initListenerAttached) return;
|
|
559
|
+
initListenerAttached = true;
|
|
560
|
+
window.addEventListener("message", (event) => {
|
|
561
|
+
if (event.source !== window.parent) return;
|
|
562
|
+
if (!event.data || typeof event.data !== "object") return;
|
|
563
|
+
if (event.data.type !== "whatalo:init") return;
|
|
564
|
+
const origin = event.data.origin;
|
|
565
|
+
if (typeof origin !== "string" || !origin) return;
|
|
566
|
+
parentOrigin = origin;
|
|
567
|
+
for (const resolve of originResolvers) {
|
|
568
|
+
resolve(origin);
|
|
569
|
+
}
|
|
570
|
+
originResolvers.length = 0;
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
function useWhataloAction() {
|
|
574
|
+
const pendingAcks = useRef(
|
|
575
|
+
/* @__PURE__ */ new Map()
|
|
576
|
+
);
|
|
577
|
+
useEffect(() => {
|
|
578
|
+
attachInitListener();
|
|
579
|
+
const handleMessage = (event) => {
|
|
580
|
+
if (!event.data || typeof event.data !== "object") return;
|
|
581
|
+
const msg = event.data;
|
|
582
|
+
if (msg.type !== "whatalo:ack") return;
|
|
583
|
+
const resolve = pendingAcks.current.get(msg.actionId);
|
|
584
|
+
if (resolve) {
|
|
585
|
+
resolve(msg);
|
|
586
|
+
pendingAcks.current.delete(msg.actionId);
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
window.addEventListener("message", handleMessage);
|
|
590
|
+
return () => window.removeEventListener("message", handleMessage);
|
|
591
|
+
}, []);
|
|
592
|
+
const sendAction = useCallback(
|
|
593
|
+
(action, payload) => {
|
|
594
|
+
return new Promise((resolve) => {
|
|
595
|
+
const actionId = crypto.randomUUID();
|
|
596
|
+
const timeout = setTimeout(() => {
|
|
597
|
+
pendingAcks.current.delete(actionId);
|
|
598
|
+
resolve({
|
|
599
|
+
type: "whatalo:ack",
|
|
600
|
+
actionId,
|
|
601
|
+
success: false,
|
|
602
|
+
error: "timeout"
|
|
603
|
+
});
|
|
604
|
+
}, ACK_TIMEOUT_MS);
|
|
605
|
+
pendingAcks.current.set(actionId, (ack) => {
|
|
606
|
+
clearTimeout(timeout);
|
|
607
|
+
resolve(ack);
|
|
608
|
+
});
|
|
609
|
+
waitForParentOrigin().then((origin) => {
|
|
610
|
+
window.parent.postMessage(
|
|
611
|
+
{ type: "whatalo:action", actionId, action, payload },
|
|
612
|
+
origin
|
|
613
|
+
);
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
},
|
|
617
|
+
[]
|
|
618
|
+
);
|
|
619
|
+
const showToast = useCallback(
|
|
620
|
+
(options) => sendAction("toast", options),
|
|
621
|
+
[sendAction]
|
|
622
|
+
);
|
|
623
|
+
const navigate = useCallback(
|
|
624
|
+
(path) => sendAction("navigate", { path }),
|
|
625
|
+
[sendAction]
|
|
626
|
+
);
|
|
627
|
+
const openModal = useCallback(
|
|
628
|
+
(options) => sendAction("modal", {
|
|
629
|
+
operation: "open",
|
|
630
|
+
...options
|
|
631
|
+
}),
|
|
632
|
+
[sendAction]
|
|
633
|
+
);
|
|
634
|
+
const closeModal = useCallback(
|
|
635
|
+
() => sendAction("modal", { operation: "close" }),
|
|
636
|
+
[sendAction]
|
|
637
|
+
);
|
|
638
|
+
const resize = useCallback(
|
|
639
|
+
(height) => sendAction("resize", { height }),
|
|
640
|
+
[sendAction]
|
|
641
|
+
);
|
|
642
|
+
const sendBillingAction = useCallback(
|
|
643
|
+
(operation, extra) => sendAction("billing", {
|
|
644
|
+
operation,
|
|
645
|
+
...extra
|
|
646
|
+
}),
|
|
647
|
+
[sendAction]
|
|
648
|
+
);
|
|
649
|
+
const billingGetPlans = useCallback(async () => {
|
|
650
|
+
const ack = await sendBillingAction(BILLING_OPERATION.GET_PLANS);
|
|
651
|
+
if (!ack.success) {
|
|
652
|
+
throw new Error(ack.error ?? "billing.getPlans failed");
|
|
653
|
+
}
|
|
654
|
+
return ack.data ?? [];
|
|
655
|
+
}, [sendBillingAction]);
|
|
656
|
+
const billingGetSubscription = useCallback(
|
|
657
|
+
async () => {
|
|
658
|
+
const ack = await sendBillingAction(BILLING_OPERATION.GET_SUBSCRIPTION);
|
|
659
|
+
if (!ack.success) {
|
|
660
|
+
throw new Error(ack.error ?? "billing.getSubscription failed");
|
|
661
|
+
}
|
|
662
|
+
return ack.data ?? null;
|
|
663
|
+
},
|
|
664
|
+
[sendBillingAction]
|
|
665
|
+
);
|
|
666
|
+
const billingRequestSubscription = useCallback(
|
|
667
|
+
async (planSlug) => {
|
|
668
|
+
const ack = await sendBillingAction(BILLING_OPERATION.REQUEST_SUBSCRIPTION, {
|
|
669
|
+
planSlug
|
|
670
|
+
});
|
|
671
|
+
if (!ack.success) {
|
|
672
|
+
throw new Error(ack.error ?? "billing.requestSubscription failed");
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
[sendBillingAction]
|
|
676
|
+
);
|
|
677
|
+
const billingCancelSubscription = useCallback(async () => {
|
|
678
|
+
const ack = await sendBillingAction(BILLING_OPERATION.CANCEL_SUBSCRIPTION);
|
|
679
|
+
if (!ack.success) {
|
|
680
|
+
throw new Error(ack.error ?? "billing.cancelSubscription failed");
|
|
681
|
+
}
|
|
682
|
+
}, [sendBillingAction]);
|
|
683
|
+
const billingReactivateSubscription = useCallback(async () => {
|
|
684
|
+
const ack = await sendBillingAction(BILLING_OPERATION.REACTIVATE_SUBSCRIPTION);
|
|
685
|
+
if (!ack.success) {
|
|
686
|
+
throw new Error(ack.error ?? "billing.reactivateSubscription failed");
|
|
687
|
+
}
|
|
688
|
+
}, [sendBillingAction]);
|
|
689
|
+
const billingSwitchPlan = useCallback(
|
|
690
|
+
async (newPlanSlug) => {
|
|
691
|
+
const ack = await sendBillingAction(BILLING_OPERATION.SWITCH_PLAN, {
|
|
692
|
+
planSlug: newPlanSlug
|
|
693
|
+
});
|
|
694
|
+
if (!ack.success) {
|
|
695
|
+
throw new Error(ack.error ?? "billing.switchPlan failed");
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
[sendBillingAction]
|
|
699
|
+
);
|
|
700
|
+
const billing = {
|
|
701
|
+
getPlans: billingGetPlans,
|
|
702
|
+
getSubscription: billingGetSubscription,
|
|
703
|
+
requestSubscription: billingRequestSubscription,
|
|
704
|
+
cancelSubscription: billingCancelSubscription,
|
|
705
|
+
reactivateSubscription: billingReactivateSubscription,
|
|
706
|
+
switchPlan: billingSwitchPlan
|
|
707
|
+
};
|
|
708
|
+
return { sendAction, showToast, navigate, openModal, closeModal, resize, billing };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// src/bridge/use-whatalo-context.ts
|
|
712
|
+
var DEFAULT_CONTEXT = {
|
|
713
|
+
storeId: "",
|
|
714
|
+
storeName: "",
|
|
715
|
+
user: { id: "", name: "", email: "", role: "owner" },
|
|
716
|
+
appId: "",
|
|
717
|
+
currentPage: "",
|
|
718
|
+
locale: "es",
|
|
719
|
+
theme: "light",
|
|
720
|
+
initialHeight: 200
|
|
721
|
+
};
|
|
722
|
+
function useWhataloContext() {
|
|
723
|
+
const [context, setContext] = useState(DEFAULT_CONTEXT);
|
|
724
|
+
const [isReady, setIsReady] = useState(false);
|
|
725
|
+
const handleMessage = useCallback2((event) => {
|
|
726
|
+
if (!event.data || typeof event.data !== "object") return;
|
|
727
|
+
const msg = event.data;
|
|
728
|
+
if (msg.type !== "whatalo:context") return;
|
|
729
|
+
const payload = msg.data ?? msg.context;
|
|
730
|
+
if (!payload || typeof payload.storeId !== "string") return;
|
|
731
|
+
setContext(payload);
|
|
732
|
+
setIsReady(true);
|
|
733
|
+
if (payload.theme) {
|
|
734
|
+
document.documentElement.setAttribute("data-theme", payload.theme);
|
|
735
|
+
}
|
|
736
|
+
}, []);
|
|
737
|
+
useEffect2(() => {
|
|
738
|
+
attachInitListener();
|
|
739
|
+
window.addEventListener("message", handleMessage);
|
|
740
|
+
waitForParentOrigin().then((origin) => {
|
|
741
|
+
window.parent.postMessage(
|
|
742
|
+
{
|
|
743
|
+
type: "whatalo:action",
|
|
744
|
+
actionId: crypto.randomUUID(),
|
|
745
|
+
action: "ready",
|
|
746
|
+
payload: {}
|
|
747
|
+
},
|
|
748
|
+
origin
|
|
749
|
+
);
|
|
750
|
+
});
|
|
751
|
+
return () => window.removeEventListener("message", handleMessage);
|
|
752
|
+
}, [handleMessage]);
|
|
753
|
+
return { ...context, isReady };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/bridge/use-app-bridge.ts
|
|
757
|
+
function useAppBridge() {
|
|
758
|
+
const ctx = useWhataloContext();
|
|
759
|
+
const actions = useWhataloAction();
|
|
760
|
+
return {
|
|
761
|
+
storeId: ctx.storeId,
|
|
762
|
+
storeName: ctx.storeName,
|
|
763
|
+
locale: ctx.locale,
|
|
764
|
+
theme: ctx.theme,
|
|
765
|
+
user: ctx.user,
|
|
766
|
+
appId: ctx.appId,
|
|
767
|
+
isReady: ctx.isReady,
|
|
768
|
+
toast: {
|
|
769
|
+
show: (title, options) => actions.showToast({ title, ...options })
|
|
770
|
+
},
|
|
771
|
+
navigate: actions.navigate,
|
|
772
|
+
modal: {
|
|
773
|
+
open: actions.openModal,
|
|
774
|
+
close: actions.closeModal
|
|
775
|
+
},
|
|
776
|
+
resize: actions.resize,
|
|
777
|
+
billing: actions.billing
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/index.ts
|
|
782
|
+
var SDK_VERSION = "0.0.1";
|
|
783
|
+
export {
|
|
784
|
+
AuthenticationError,
|
|
785
|
+
AuthorizationError,
|
|
786
|
+
BILLING_OPERATION,
|
|
787
|
+
InternalError,
|
|
788
|
+
ManifestValidationError,
|
|
789
|
+
NotFoundError,
|
|
790
|
+
RateLimitError,
|
|
791
|
+
SDK_VERSION,
|
|
792
|
+
ValidationError,
|
|
793
|
+
WhataloAPIError,
|
|
794
|
+
WhataloClient,
|
|
795
|
+
defineApp,
|
|
796
|
+
useAppBridge,
|
|
797
|
+
useWhataloAction,
|
|
798
|
+
useWhataloContext,
|
|
799
|
+
verifyWebhook
|
|
800
|
+
};
|
|
801
|
+
//# sourceMappingURL=index.mjs.map
|