@blamejs/blamejs-shop 0.0.65 → 0.0.70

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/assembly-instructions.js +777 -0
  3. package/lib/auto-replenish.js +933 -0
  4. package/lib/business-hours.js +980 -0
  5. package/lib/click-and-collect.js +711 -0
  6. package/lib/clickstream.js +713 -0
  7. package/lib/cost-layers.js +774 -0
  8. package/lib/credit-limits.js +752 -0
  9. package/lib/currency-rounding.js +525 -0
  10. package/lib/customer-activity.js +862 -0
  11. package/lib/customer-notes.js +712 -0
  12. package/lib/customer-risk-profile.js +593 -0
  13. package/lib/customer-surveys.js +1012 -0
  14. package/lib/damage-photos.js +473 -0
  15. package/lib/discount-allocation.js +557 -0
  16. package/lib/dropship-forwarding.js +645 -0
  17. package/lib/email-templates.js +817 -0
  18. package/lib/index.js +45 -0
  19. package/lib/inventory-allocations.js +559 -0
  20. package/lib/inventory-writeoffs.js +636 -0
  21. package/lib/knowledge-base.js +1104 -0
  22. package/lib/locale-router.js +1077 -0
  23. package/lib/operator-roles.js +768 -0
  24. package/lib/order-escalation.js +951 -0
  25. package/lib/order-ratings.js +495 -0
  26. package/lib/order-tags.js +944 -0
  27. package/lib/packing-slips.js +810 -0
  28. package/lib/payment-retries.js +816 -0
  29. package/lib/pick-lists.js +639 -0
  30. package/lib/pixel-events.js +995 -0
  31. package/lib/preorder.js +595 -0
  32. package/lib/print-queue.js +681 -0
  33. package/lib/product-qa.js +749 -0
  34. package/lib/promo-bundles.js +835 -0
  35. package/lib/push-notifications.js +937 -0
  36. package/lib/refund-automation.js +853 -0
  37. package/lib/reorder-reminders.js +798 -0
  38. package/lib/robots-config.js +753 -0
  39. package/lib/seller-signup.js +1052 -0
  40. package/lib/site-redirects.js +690 -0
  41. package/lib/sitemap-generator.js +717 -0
  42. package/lib/subscription-gifts.js +710 -0
  43. package/lib/tax-cert-renewals.js +632 -0
  44. package/lib/theme-assets.js +711 -0
  45. package/lib/tier-benefits.js +776 -0
  46. package/lib/vendor/MANIFEST.json +2 -2
  47. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  48. package/lib/vendor/blamejs/api-snapshot.json +2 -2
  49. package/lib/vendor/blamejs/lib/metrics.js +68 -4
  50. package/lib/vendor/blamejs/package.json +1 -1
  51. package/lib/vendor/blamejs/release-notes/v0.12.5.json +40 -0
  52. package/lib/wishlist-alerts.js +842 -0
  53. package/lib/wishlist-sharing.js +718 -0
  54. package/package.json +1 -1
@@ -0,0 +1,995 @@
1
+ "use strict";
2
+ /**
3
+ * @module shop.pixelEvents
4
+ * @title Pixel events — server-side conversion-tracking pixel/event
5
+ * API integration.
6
+ *
7
+ * @intro
8
+ * Every modern ad platform exposes a server-side Conversions API
9
+ * (Meta CAPI, Google Enhanced Conversions, TikTok Events API,
10
+ * Pinterest CAPI, Snap CAPI). Operators register one provider row
11
+ * per platform; the storefront calls `recordEvent` at every
12
+ * purchase / add-to-cart / view / lead / signup / search; an
13
+ * operator-wired worker drains the queued rows by HTTPS-POSTing
14
+ * each event to the platform's conversion URL and routes the
15
+ * response back through `markDispatched` / `markFailed`.
16
+ *
17
+ * The shape:
18
+ *
19
+ * var pe = bShop.pixelEvents.create({ query: q });
20
+ *
21
+ * await pe.registerProvider({
22
+ * slug: "meta",
23
+ * provider: "meta_capi",
24
+ * pixel_id: "1234567890",
25
+ * access_token: "EAAB...redacted",
26
+ * conversion_url: "https://graph.facebook.com/v18.0/1234567890/events",
27
+ * active: true,
28
+ * });
29
+ *
30
+ * await pe.recordEvent({
31
+ * provider_slug: "meta",
32
+ * event_name: "purchase",
33
+ * event_id: "order_8819_purchase",
34
+ * occurred_at: Date.now(),
35
+ * customer_email: "Alice@Example.COM", // hashed on the way in
36
+ * customer_phone: "+15555550123", // hashed on the way in
37
+ * order_id: orderId,
38
+ * value_minor: 2999,
39
+ * currency: "USD",
40
+ * event_source_url: "https://shop.example/orders/8819/thanks",
41
+ * });
42
+ *
43
+ * // Scheduler tick — operator's worker pulls due rows, POSTs
44
+ * // each to the provider's conversion URL, then routes the
45
+ * // result back into the FSM.
46
+ * var due = await pe.dispatchTick({ now: Date.now(), batch_size: 50 });
47
+ * for (var i = 0; i < due.length; i += 1) {
48
+ * var ev = due[i];
49
+ * try {
50
+ * var res = await fetch(<provider.conversion_url>, { ... });
51
+ * await pe.markDispatched({
52
+ * event_id: ev.id,
53
+ * response_status: res.status,
54
+ * response_body: await res.text(),
55
+ * });
56
+ * } catch (err) {
57
+ * await pe.markFailed({
58
+ * event_id: ev.id,
59
+ * reason: err && err.message || "dispatch_error",
60
+ * retry: true,
61
+ * });
62
+ * }
63
+ * }
64
+ *
65
+ * PII handling:
66
+ * `customer_email` / `customer_phone` are normalised + SHA-256
67
+ * hashed at the write site. The plaintext NEVER reaches the
68
+ * pixel_events row — every supported provider expects the
69
+ * identity-matching column to arrive as a hex-encoded SHA-256
70
+ * digest of the normalised value, and the framework refuses to
71
+ * persist the raw form. Email normalisation lowercases + trims;
72
+ * phone normalisation strips every non-digit byte plus a leading
73
+ * `+`. The resulting bytes are SHA-256-hashed via node:crypto.
74
+ * `ip_hash` is operator-supplied (already hashed by the caller —
75
+ * the primitive doesn't model IPv4/IPv6 normalisation here).
76
+ *
77
+ * FSM:
78
+ * queued → dispatched
79
+ * ↘ failed (retry → queued | terminal)
80
+ *
81
+ * Idempotency:
82
+ * `event_id` is the operator's dedup token — every supported
83
+ * platform uses an `event_id` field for de-duplication on their
84
+ * end. The (provider_slug, event_id) UNIQUE index returns the
85
+ * existing row when a retry replays.
86
+ *
87
+ * Storage: migration 0123_pixel_events.sql.
88
+ *
89
+ * Composition: zero npm runtime deps. The primitive composes
90
+ * blamejs (`b.uuid.v7`, `b.guardUuid.sanitize`) and node:crypto for
91
+ * the SHA-256 hashing the provider spec mandates — the framework's
92
+ * `b.crypto.namespaceHash` is SHA3-512 + prefix, which doesn't
93
+ * match what Meta / Google / TikTok / Pinterest / Snap expect.
94
+ *
95
+ * @primitive pixelEvents
96
+ * @related b.uuid, b.guardUuid
97
+ */
98
+
99
+ var nodeCrypto = require("node:crypto"); // allow:non-shop-require — Provider spec (Meta CAPI, Google EC, TikTok Events, Pinterest CAPI, Snap CAPI) mandates SHA-256 of the normalised identifier; blamejs.crypto exposes SHA3-512 / SHAKE256 / namespaceHash, none of which match the wire format ad platforms accept. Node's stdlib createHash("sha256") is the minimum-surface route.
100
+
101
+ var bShop;
102
+ function _b() {
103
+ if (!bShop) bShop = require("./index");
104
+ return bShop.framework;
105
+ }
106
+
107
+ // ---- constants ----------------------------------------------------------
108
+
109
+ var PROVIDERS = ["meta_capi", "google_ec", "tiktok_events", "pinterest_capi", "snap_capi"];
110
+ var EVENT_NAMES = [
111
+ "purchase", "add_to_cart", "view_content",
112
+ "lead", "complete_registration", "search",
113
+ ];
114
+ var STATUSES = ["queued", "dispatched", "failed"];
115
+
116
+ var MAX_SLUG_LEN = 64;
117
+ var MAX_PIXEL_ID_LEN = 128;
118
+ var MAX_ACCESS_TOKEN_LEN = 2048;
119
+ var MAX_URL_LEN = 2048;
120
+ var MAX_EVENT_ID_LEN = 128;
121
+ var MAX_CURRENCY_LEN = 3;
122
+ var MAX_UA_CLASS_LEN = 64;
123
+ var MAX_IP_HASH_LEN = 256;
124
+ var MAX_REASON_LEN = 1024;
125
+ var MAX_RESPONSE_BODY_LEN = 16384;
126
+ var MAX_VALUE_MINOR = 1000000000000; // 1 trillion minor units — well above any single conversion
127
+ var MAX_LIST_LIMIT = 500;
128
+ var DEFAULT_LIST_LIMIT = 100;
129
+ var MAX_BATCH_SIZE = 1000;
130
+ var DEFAULT_BATCH_SIZE = 100;
131
+
132
+ var SLUG_RE = /^[a-z](?:[a-z0-9-]*[a-z0-9])?$/;
133
+ var EVENT_ID_RE = /^[A-Za-z0-9._:-]+$/;
134
+ var CURRENCY_RE = /^[A-Z]{3}$/;
135
+ var UA_CLASS_RE = /^[a-z0-9._-]+$/;
136
+ var IP_HASH_RE = /^[0-9a-f]+$/;
137
+ var SHA256_HEX_RE = /^[0-9a-f]{64}$/;
138
+ var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
139
+ var ZERO_WIDTH_RE = new RegExp(
140
+ "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
141
+ );
142
+ // Conservative email shape — operator-facing storefronts already
143
+ // validated this via the framework's address-input primitive; the
144
+ // gate here is a defensive last-line check before we normalise +
145
+ // hash. RFC 5321 caps the local-part at 64 chars and the full
146
+ // address at 254.
147
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
148
+ var MAX_EMAIL_LEN = 254;
149
+ var MAX_PHONE_LEN = 32;
150
+ // E.164 textual phone — `+` + 7-15 digits — same shape the SMS
151
+ // dispatcher uses. The normaliser strips non-digits + a leading `+`
152
+ // before hashing; the validator gates the input before normalisation.
153
+ var PHONE_RAW_RE = /^\+?[0-9 ().\-]{7,32}$/;
154
+
155
+ // Transient-failure retry back-off — 1m, 5m, 30m, 2h, 12h. The
156
+ // dispatch tick consults the schedule on `markFailed({ retry: true })`
157
+ // to compute the next attempt window. Once the schedule is exhausted
158
+ // the row terminates as failed regardless of the caller's `retry`
159
+ // request — runaway retries against a permanently-broken provider
160
+ // credential would otherwise saturate the worker tick.
161
+ var RETRY_BACKOFF_MS = [
162
+ 60 * 1000,
163
+ 5 * 60 * 1000,
164
+ 30 * 60 * 1000,
165
+ 2 * 60 * 60 * 1000,
166
+ 12 * 60 * 60 * 1000,
167
+ ];
168
+
169
+ // ---- monotonic clock ---------------------------------------------------
170
+ //
171
+ // Wall-clock can stall on a fast hot loop (multiple inserts inside
172
+ // the same millisecond) and on coarse-grained virtualised hosts. The
173
+ // monotonic shim keeps the per-process `created_at` / `updated_at`
174
+ // timestamps strictly increasing — every subsequent call observes a
175
+ // timestamp at least 1ms greater than the previous one. Sibling
176
+ // primitives (smsDispatcher, et al.) use the same shape; the FSM
177
+ // lookups + dispatchTick ordering rely on strict monotonicity to
178
+ // break ties deterministically.
179
+
180
+ var _lastTs = 0;
181
+ function _now() {
182
+ var t = Date.now();
183
+ if (t <= _lastTs) { t = _lastTs + 1; }
184
+ _lastTs = t;
185
+ return t;
186
+ }
187
+
188
+ // ---- validators ---------------------------------------------------------
189
+
190
+ function _slug(s, label) {
191
+ if (typeof s !== "string" || !s.length) {
192
+ throw new TypeError("pixelEvents: " + label + " must be a non-empty string");
193
+ }
194
+ if (s.length > MAX_SLUG_LEN) {
195
+ throw new TypeError("pixelEvents: " + label + " must be <= " + MAX_SLUG_LEN + " characters");
196
+ }
197
+ if (!SLUG_RE.test(s)) {
198
+ throw new TypeError("pixelEvents: " + label + " must match /[a-z][a-z0-9-]*[a-z0-9]/");
199
+ }
200
+ return s;
201
+ }
202
+
203
+ function _provider(s) {
204
+ if (typeof s !== "string" || PROVIDERS.indexOf(s) === -1) {
205
+ throw new TypeError("pixelEvents: provider must be one of " + PROVIDERS.join(", "));
206
+ }
207
+ return s;
208
+ }
209
+
210
+ function _eventName(s) {
211
+ if (typeof s !== "string" || EVENT_NAMES.indexOf(s) === -1) {
212
+ throw new TypeError("pixelEvents: event_name must be one of " + EVENT_NAMES.join(", "));
213
+ }
214
+ return s;
215
+ }
216
+
217
+ function _pixelId(s) {
218
+ if (typeof s !== "string" || !s.length) {
219
+ throw new TypeError("pixelEvents: pixel_id must be a non-empty string");
220
+ }
221
+ if (s.length > MAX_PIXEL_ID_LEN) {
222
+ throw new TypeError("pixelEvents: pixel_id must be <= " + MAX_PIXEL_ID_LEN + " characters");
223
+ }
224
+ if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
225
+ throw new TypeError("pixelEvents: pixel_id contains control / zero-width bytes");
226
+ }
227
+ return s;
228
+ }
229
+
230
+ function _accessToken(s) {
231
+ if (typeof s !== "string" || !s.length) {
232
+ throw new TypeError("pixelEvents: access_token must be a non-empty string");
233
+ }
234
+ if (s.length > MAX_ACCESS_TOKEN_LEN) {
235
+ throw new TypeError("pixelEvents: access_token must be <= " + MAX_ACCESS_TOKEN_LEN + " characters");
236
+ }
237
+ if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
238
+ throw new TypeError("pixelEvents: access_token contains control / zero-width bytes");
239
+ }
240
+ return s;
241
+ }
242
+
243
+ function _conversionUrl(s) {
244
+ if (typeof s !== "string" || !s.length) {
245
+ throw new TypeError("pixelEvents: conversion_url must be a non-empty string");
246
+ }
247
+ if (s.length > MAX_URL_LEN) {
248
+ throw new TypeError("pixelEvents: conversion_url must be <= " + MAX_URL_LEN + " characters");
249
+ }
250
+ if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
251
+ throw new TypeError("pixelEvents: conversion_url contains control / zero-width bytes");
252
+ }
253
+ // Every supported provider's conversion API is public HTTPS — refuse
254
+ // anything else so an operator typo can't egress conversion data in
255
+ // plaintext.
256
+ if (!/^https:\/\//i.test(s)) {
257
+ throw new TypeError("pixelEvents: conversion_url must be an absolute https:// URL");
258
+ }
259
+ return s;
260
+ }
261
+
262
+ function _eventId(s) {
263
+ if (typeof s !== "string" || !s.length) {
264
+ throw new TypeError("pixelEvents: event_id must be a non-empty string");
265
+ }
266
+ if (s.length > MAX_EVENT_ID_LEN) {
267
+ throw new TypeError("pixelEvents: event_id must be <= " + MAX_EVENT_ID_LEN + " characters");
268
+ }
269
+ if (!EVENT_ID_RE.test(s)) {
270
+ throw new TypeError("pixelEvents: event_id must match /^[A-Za-z0-9._:-]+$/");
271
+ }
272
+ return s;
273
+ }
274
+
275
+ function _eventSourceUrl(s) {
276
+ if (typeof s !== "string" || !s.length) {
277
+ throw new TypeError("pixelEvents: event_source_url must be a non-empty string");
278
+ }
279
+ if (s.length > MAX_URL_LEN) {
280
+ throw new TypeError("pixelEvents: event_source_url must be <= " + MAX_URL_LEN + " characters");
281
+ }
282
+ if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
283
+ throw new TypeError("pixelEvents: event_source_url contains control / zero-width bytes");
284
+ }
285
+ if (!/^https?:\/\//i.test(s)) {
286
+ throw new TypeError("pixelEvents: event_source_url must be an absolute http(s):// URL");
287
+ }
288
+ return s;
289
+ }
290
+
291
+ function _email(s) {
292
+ if (s == null) return null;
293
+ if (typeof s !== "string" || !s.length) {
294
+ throw new TypeError("pixelEvents: customer_email must be a non-empty string when provided");
295
+ }
296
+ if (s.length > MAX_EMAIL_LEN) {
297
+ throw new TypeError("pixelEvents: customer_email must be <= " + MAX_EMAIL_LEN + " characters");
298
+ }
299
+ if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
300
+ throw new TypeError("pixelEvents: customer_email contains control / zero-width bytes");
301
+ }
302
+ // Validate the trimmed form — normalisation strips leading /
303
+ // trailing whitespace before hashing, so a " alice@example.com "
304
+ // input still hashes to the canonical address. Reject only when
305
+ // the trimmed form doesn't look like an email at all.
306
+ if (!EMAIL_RE.test(s.trim())) {
307
+ throw new TypeError("pixelEvents: customer_email must be a valid address shape");
308
+ }
309
+ return s;
310
+ }
311
+
312
+ function _phone(s) {
313
+ if (s == null) return null;
314
+ if (typeof s !== "string" || !s.length) {
315
+ throw new TypeError("pixelEvents: customer_phone must be a non-empty string when provided");
316
+ }
317
+ if (s.length > MAX_PHONE_LEN) {
318
+ throw new TypeError("pixelEvents: customer_phone must be <= " + MAX_PHONE_LEN + " characters");
319
+ }
320
+ if (!PHONE_RAW_RE.test(s)) {
321
+ throw new TypeError("pixelEvents: customer_phone must contain only digits + optional leading + / spaces / dashes / parens");
322
+ }
323
+ return s;
324
+ }
325
+
326
+ function _currency(s) {
327
+ if (s == null) return null;
328
+ if (typeof s !== "string") {
329
+ throw new TypeError("pixelEvents: currency must be a string when provided");
330
+ }
331
+ if (s.length !== MAX_CURRENCY_LEN || !CURRENCY_RE.test(s)) {
332
+ throw new TypeError("pixelEvents: currency must be a 3-letter uppercase ISO-4217 code (e.g. 'USD')");
333
+ }
334
+ return s;
335
+ }
336
+
337
+ function _valueMinor(n) {
338
+ if (n == null) return null;
339
+ if (!Number.isInteger(n) || n < 0 || n > MAX_VALUE_MINOR) {
340
+ throw new TypeError("pixelEvents: value_minor must be a non-negative integer <= " + MAX_VALUE_MINOR);
341
+ }
342
+ return n;
343
+ }
344
+
345
+ function _uaClass(s) {
346
+ if (s == null) return null;
347
+ if (typeof s !== "string" || !s.length) {
348
+ throw new TypeError("pixelEvents: ua_class must be a non-empty string when provided");
349
+ }
350
+ if (s.length > MAX_UA_CLASS_LEN) {
351
+ throw new TypeError("pixelEvents: ua_class must be <= " + MAX_UA_CLASS_LEN + " characters");
352
+ }
353
+ if (!UA_CLASS_RE.test(s)) {
354
+ throw new TypeError("pixelEvents: ua_class must match /[a-z0-9._-]+/");
355
+ }
356
+ return s;
357
+ }
358
+
359
+ function _ipHash(s) {
360
+ if (s == null) return null;
361
+ if (typeof s !== "string" || !s.length) {
362
+ throw new TypeError("pixelEvents: ip_hash must be a non-empty string when provided");
363
+ }
364
+ if (s.length > MAX_IP_HASH_LEN) {
365
+ throw new TypeError("pixelEvents: ip_hash must be <= " + MAX_IP_HASH_LEN + " characters");
366
+ }
367
+ if (!IP_HASH_RE.test(s)) {
368
+ throw new TypeError("pixelEvents: ip_hash must be lowercase hex");
369
+ }
370
+ return s;
371
+ }
372
+
373
+ function _epochMs(n, label) {
374
+ if (!Number.isInteger(n) || n <= 0) {
375
+ throw new TypeError("pixelEvents: " + label + " must be a positive integer (epoch ms)");
376
+ }
377
+ return n;
378
+ }
379
+
380
+ function _optionalEpochMs(n, label) {
381
+ if (n == null) return null;
382
+ return _epochMs(n, label);
383
+ }
384
+
385
+ function _limit(n, label) {
386
+ if (n == null) return DEFAULT_LIST_LIMIT;
387
+ if (!Number.isInteger(n) || n <= 0 || n > MAX_LIST_LIMIT) {
388
+ throw new TypeError("pixelEvents: " + label + " must be an integer in [1, " + MAX_LIST_LIMIT + "]");
389
+ }
390
+ return n;
391
+ }
392
+
393
+ function _batchSize(n) {
394
+ if (n == null) return DEFAULT_BATCH_SIZE;
395
+ if (!Number.isInteger(n) || n <= 0 || n > MAX_BATCH_SIZE) {
396
+ throw new TypeError("pixelEvents: batch_size must be an integer in [1, " + MAX_BATCH_SIZE + "]");
397
+ }
398
+ return n;
399
+ }
400
+
401
+ function _reason(s, label) {
402
+ if (typeof s !== "string" || !s.length) {
403
+ throw new TypeError("pixelEvents: " + label + " must be a non-empty string");
404
+ }
405
+ if (s.length > MAX_REASON_LEN) {
406
+ throw new TypeError("pixelEvents: " + label + " must be <= " + MAX_REASON_LEN + " characters");
407
+ }
408
+ if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
409
+ throw new TypeError("pixelEvents: " + label + " contains control / zero-width bytes");
410
+ }
411
+ return s;
412
+ }
413
+
414
+ function _responseBody(s) {
415
+ if (s == null) return null;
416
+ if (typeof s !== "string") {
417
+ throw new TypeError("pixelEvents: response_body must be a string when provided");
418
+ }
419
+ if (s.length > MAX_RESPONSE_BODY_LEN) {
420
+ throw new TypeError("pixelEvents: response_body must be <= " + MAX_RESPONSE_BODY_LEN + " characters");
421
+ }
422
+ // Response bodies from upstream providers are operator-observed
423
+ // already (the dispatch hook captured them); allow the full ASCII
424
+ // range minus the zero-width / bidi block so a malicious provider
425
+ // can't smuggle invisible bytes through the audit trail.
426
+ if (ZERO_WIDTH_RE.test(s)) {
427
+ throw new TypeError("pixelEvents: response_body contains zero-width / bidi-override bytes");
428
+ }
429
+ return s;
430
+ }
431
+
432
+ function _responseStatus(n) {
433
+ if (!Number.isInteger(n) || n < 100 || n > 599) {
434
+ throw new TypeError("pixelEvents: response_status must be an integer HTTP status in [100, 599]");
435
+ }
436
+ return n;
437
+ }
438
+
439
+ function _optionalUuid(s, label) {
440
+ if (s == null) return null;
441
+ try { return _b().guardUuid.sanitize(s, { profile: "strict" }); }
442
+ catch (e) {
443
+ throw new TypeError("pixelEvents: " + label + " — " + (e && e.message || "invalid UUID"));
444
+ }
445
+ }
446
+
447
+ function _uuid(s, label) {
448
+ try { return _b().guardUuid.sanitize(s, { profile: "strict" }); }
449
+ catch (e) {
450
+ throw new TypeError("pixelEvents: " + label + " — " + (e && e.message || "invalid UUID"));
451
+ }
452
+ }
453
+
454
+ // ---- normalisation + hashing -------------------------------------------
455
+ //
456
+ // Each platform's identity-matching surface accepts a hex-encoded
457
+ // SHA-256 of the normalised identifier. Normalisation rules are
458
+ // platform-portable (the five providers we support converge on the
459
+ // same shape):
460
+ //
461
+ // email — trim leading/trailing whitespace, lowercase.
462
+ // phone — strip every non-digit byte. A leading `+` is implicit;
463
+ // the digits themselves carry the country code (the operator
464
+ // is expected to pass E.164-ish input — `+15555550123` /
465
+ // `1 (555) 555-0123` / `+1.555.555.0123` all normalise to
466
+ // `15555550123`).
467
+ //
468
+ // The output is a 64-char lowercase hex digest. Empty input (after
469
+ // normalisation) returns null — a caller that hands " " or "+" as a
470
+ // phone hasn't actually supplied an identifier.
471
+
472
+ function _normaliseEmail(s) {
473
+ return String(s).trim().toLowerCase();
474
+ }
475
+
476
+ function _normalisePhone(s) {
477
+ return String(s).replace(/[^0-9]/g, "");
478
+ }
479
+
480
+ function _sha256Hex(s) {
481
+ return nodeCrypto.createHash("sha256").update(s, "utf8").digest("hex");
482
+ }
483
+
484
+ function _hashEmail(raw) {
485
+ if (raw == null) return null;
486
+ var norm = _normaliseEmail(raw);
487
+ if (!norm.length) return null;
488
+ return _sha256Hex(norm);
489
+ }
490
+
491
+ function _hashPhone(raw) {
492
+ if (raw == null) return null;
493
+ var norm = _normalisePhone(raw);
494
+ if (!norm.length) return null;
495
+ return _sha256Hex(norm);
496
+ }
497
+
498
+ // ---- row hydration ------------------------------------------------------
499
+
500
+ function _hydrateProvider(r) {
501
+ if (!r) return null;
502
+ return {
503
+ slug: r.slug,
504
+ provider: r.provider,
505
+ pixel_id: r.pixel_id,
506
+ access_token_hash: r.access_token_hash,
507
+ access_token_normalized: r.access_token_normalized,
508
+ conversion_url: r.conversion_url,
509
+ active: Number(r.active) === 1,
510
+ archived_at: r.archived_at == null ? null : Number(r.archived_at),
511
+ created_at: Number(r.created_at),
512
+ updated_at: Number(r.updated_at),
513
+ };
514
+ }
515
+
516
+ function _hydrateEvent(r) {
517
+ if (!r) return null;
518
+ return {
519
+ id: r.id,
520
+ provider_slug: r.provider_slug,
521
+ event_name: r.event_name,
522
+ event_id: r.event_id,
523
+ occurred_at: Number(r.occurred_at),
524
+ customer_email_sha256: r.customer_email_sha256 == null ? null : r.customer_email_sha256,
525
+ customer_phone_sha256: r.customer_phone_sha256 == null ? null : r.customer_phone_sha256,
526
+ customer_id: r.customer_id == null ? null : r.customer_id,
527
+ order_id: r.order_id == null ? null : r.order_id,
528
+ value_minor: r.value_minor == null ? null : Number(r.value_minor),
529
+ currency: r.currency == null ? null : r.currency,
530
+ event_source_url: r.event_source_url,
531
+ ua_class: r.ua_class == null ? null : r.ua_class,
532
+ ip_hash: r.ip_hash == null ? null : r.ip_hash,
533
+ status: r.status,
534
+ attempts: Number(r.attempts || 0),
535
+ dispatched_at: r.dispatched_at == null ? null : Number(r.dispatched_at),
536
+ response_status: r.response_status == null ? null : Number(r.response_status),
537
+ response_body: r.response_body == null ? null : r.response_body,
538
+ last_failure: r.last_failure == null ? null : r.last_failure,
539
+ created_at: Number(r.created_at),
540
+ };
541
+ }
542
+
543
+ // ---- factory ------------------------------------------------------------
544
+
545
+ function create(opts) {
546
+ opts = opts || {};
547
+ var query = opts.query;
548
+ if (!query) {
549
+ query = function (sql, params) { return _b().externalDb.query(sql, params); };
550
+ }
551
+
552
+ async function _getProvider(slug) {
553
+ var r = await query("SELECT * FROM pixel_providers WHERE slug = ?1", [slug]);
554
+ return r.rows[0] || null;
555
+ }
556
+
557
+ async function _getEvent(id) {
558
+ var r = await query("SELECT * FROM pixel_events WHERE id = ?1", [id]);
559
+ return r.rows[0] || null;
560
+ }
561
+
562
+ async function _getEventByDedup(providerSlug, eventId) {
563
+ var r = await query(
564
+ "SELECT * FROM pixel_events WHERE provider_slug = ?1 AND event_id = ?2",
565
+ [providerSlug, eventId],
566
+ );
567
+ return r.rows[0] || null;
568
+ }
569
+
570
+ // -- registerProvider --------------------------------------------------
571
+
572
+ async function registerProvider(input) {
573
+ if (!input || typeof input !== "object") {
574
+ throw new TypeError("pixelEvents.registerProvider: input object required");
575
+ }
576
+ var slug = _slug(input.slug, "slug");
577
+ var provider = _provider(input.provider);
578
+ var pixelId = _pixelId(input.pixel_id);
579
+ var accessToken = _accessToken(input.access_token);
580
+ var conversionUrl = _conversionUrl(input.conversion_url);
581
+ var active = input.active == null ? true : !!input.active;
582
+ var now = _now();
583
+ var tokenHash = _sha256Hex(accessToken);
584
+
585
+ var existing = await _getProvider(slug);
586
+ if (existing) {
587
+ // Refuse the provider-platform swap — operators that want a
588
+ // different ad platform for the same operator-facing slug must
589
+ // archive the old row and pick a new slug, otherwise the event
590
+ // ledger's historical `provider_slug` references become
591
+ // ambiguous. Field updates (pixel_id, access_token,
592
+ // conversion_url, active) are fine.
593
+ if (existing.provider !== provider) {
594
+ throw new TypeError(
595
+ "pixelEvents.registerProvider: cannot change provider platform from " +
596
+ JSON.stringify(existing.provider) + " to " + JSON.stringify(provider) +
597
+ " for slug " + JSON.stringify(slug) +
598
+ " — archive the existing provider and pick a new slug"
599
+ );
600
+ }
601
+ await query(
602
+ "UPDATE pixel_providers SET pixel_id = ?1, access_token_hash = ?2, " +
603
+ "access_token_normalized = ?3, conversion_url = ?4, active = ?5, " +
604
+ "updated_at = ?6 WHERE slug = ?7",
605
+ [pixelId, tokenHash, accessToken, conversionUrl, active ? 1 : 0, now, slug],
606
+ );
607
+ } else {
608
+ await query(
609
+ "INSERT INTO pixel_providers " +
610
+ "(slug, provider, pixel_id, access_token_hash, access_token_normalized, " +
611
+ " conversion_url, active, archived_at, created_at, updated_at) " +
612
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, ?8, ?8)",
613
+ [slug, provider, pixelId, tokenHash, accessToken, conversionUrl, active ? 1 : 0, now],
614
+ );
615
+ }
616
+ return _hydrateProvider(await _getProvider(slug));
617
+ }
618
+
619
+ // -- recordEvent -------------------------------------------------------
620
+
621
+ async function recordEvent(input) {
622
+ if (!input || typeof input !== "object") {
623
+ throw new TypeError("pixelEvents.recordEvent: input object required");
624
+ }
625
+ var providerSlug = _slug(input.provider_slug, "provider_slug");
626
+ var eventName = _eventName(input.event_name);
627
+ var eventId = _eventId(input.event_id);
628
+ var occurredAt = _epochMs(input.occurred_at, "occurred_at");
629
+ var customerEmail = _email(input.customer_email == null ? null : input.customer_email);
630
+ var customerPhone = _phone(input.customer_phone == null ? null : input.customer_phone);
631
+ var customerId = _optionalUuid(input.customer_id, "customer_id");
632
+ var orderId = _optionalUuid(input.order_id, "order_id");
633
+ var valueMinor = _valueMinor(input.value_minor == null ? null : input.value_minor);
634
+ var currency = _currency(input.currency == null ? null : input.currency);
635
+ var eventSourceUrl = _eventSourceUrl(input.event_source_url);
636
+ var uaClass = _uaClass(input.ua_class == null ? null : input.ua_class);
637
+ var ipHash = _ipHash(input.ip_hash == null ? null : input.ip_hash);
638
+
639
+ // Value + currency are joint — a purchase with a value must
640
+ // declare its currency, and a currency-less value is meaningless.
641
+ if ((valueMinor == null) !== (currency == null)) {
642
+ throw new TypeError("pixelEvents.recordEvent: value_minor and currency must be supplied together");
643
+ }
644
+
645
+ var provider = await _getProvider(providerSlug);
646
+ if (!provider) {
647
+ var pErr = new Error("pixelEvents.recordEvent: provider_slug " + JSON.stringify(providerSlug) + " not found");
648
+ pErr.code = "PROVIDER_NOT_FOUND";
649
+ throw pErr;
650
+ }
651
+
652
+ // Idempotent insert — same (provider_slug, event_id) replays
653
+ // surface the existing row instead of returning a DB UNIQUE
654
+ // violation. Every supported platform's Conversions API expects
655
+ // an `event_id` for de-duplication; the framework mirrors that
656
+ // contract on the way in so a retried storefront request doesn't
657
+ // accumulate duplicate rows.
658
+ var dup = await _getEventByDedup(providerSlug, eventId);
659
+ if (dup) return _hydrateEvent(dup);
660
+
661
+ var emailHash = _hashEmail(customerEmail);
662
+ var phoneHash = _hashPhone(customerPhone);
663
+ var now = _now();
664
+ var id = _b().uuid.v7();
665
+
666
+ await query(
667
+ "INSERT INTO pixel_events " +
668
+ "(id, provider_slug, event_name, event_id, occurred_at, " +
669
+ " customer_email_sha256, customer_phone_sha256, customer_id, order_id, " +
670
+ " value_minor, currency, event_source_url, ua_class, ip_hash, " +
671
+ " status, attempts, dispatched_at, response_status, response_body, " +
672
+ " last_failure, created_at) " +
673
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, " +
674
+ " 'queued', 0, NULL, NULL, NULL, NULL, ?15)",
675
+ [
676
+ id, providerSlug, eventName, eventId, occurredAt,
677
+ emailHash, phoneHash, customerId, orderId,
678
+ valueMinor, currency, eventSourceUrl, uaClass, ipHash,
679
+ now,
680
+ ],
681
+ );
682
+ return _hydrateEvent(await _getEvent(id));
683
+ }
684
+
685
+ // -- dispatchTick ------------------------------------------------------
686
+ //
687
+ // Scheduler-callable. Returns every queued event whose
688
+ // `occurred_at` is due AND whose provider is active. The caller's
689
+ // worker dispatches each row to the provider's conversion URL and
690
+ // routes the response back through `markDispatched` /
691
+ // `markFailed`. The tick itself doesn't mutate FSM state — that
692
+ // contract sits with the caller so a misfiring worker can replay
693
+ // a tick without double-dispatching.
694
+
695
+ async function dispatchTick(input) {
696
+ input = input || {};
697
+ var now = input.now == null ? _now() : _epochMs(input.now, "now");
698
+ var batchSize = _batchSize(input.batch_size);
699
+
700
+ var due = await query(
701
+ "SELECT e.* FROM pixel_events e " +
702
+ "JOIN pixel_providers p ON p.slug = e.provider_slug " +
703
+ "WHERE e.status = 'queued' AND e.occurred_at <= ?1 AND p.active = 1 " +
704
+ "ORDER BY e.occurred_at ASC, e.id ASC LIMIT ?2",
705
+ [now, batchSize],
706
+ );
707
+ var out = [];
708
+ for (var i = 0; i < due.rows.length; i += 1) out.push(_hydrateEvent(due.rows[i]));
709
+ return out;
710
+ }
711
+
712
+ // -- markDispatched ----------------------------------------------------
713
+
714
+ async function markDispatched(input) {
715
+ if (!input || typeof input !== "object") {
716
+ throw new TypeError("pixelEvents.markDispatched: input object required");
717
+ }
718
+ var eventId = _uuid(input.event_id, "event_id");
719
+ var responseStatus = _responseStatus(input.response_status);
720
+ var responseBody = _responseBody(input.response_body == null ? null : input.response_body);
721
+ var now = _now();
722
+
723
+ var existing = await _getEvent(eventId);
724
+ if (!existing) {
725
+ var nfErr = new Error("pixelEvents.markDispatched: event " + eventId + " not found");
726
+ nfErr.code = "EVENT_NOT_FOUND";
727
+ throw nfErr;
728
+ }
729
+ if (existing.status === "dispatched") {
730
+ // Idempotent — the worker's response handler can replay the same
731
+ // success transition.
732
+ return _hydrateEvent(existing);
733
+ }
734
+ if (existing.status === "failed") {
735
+ var fErr = new Error(
736
+ "pixelEvents.markDispatched: refusing dispatched transition from terminal failed"
737
+ );
738
+ fErr.code = "FSM_TERMINAL";
739
+ throw fErr;
740
+ }
741
+ var attempts = Number(existing.attempts || 0) + 1;
742
+ await query(
743
+ "UPDATE pixel_events SET status = 'dispatched', attempts = ?1, " +
744
+ "dispatched_at = ?2, response_status = ?3, response_body = ?4, " +
745
+ "last_failure = NULL WHERE id = ?5",
746
+ [attempts, now, responseStatus, responseBody, eventId],
747
+ );
748
+ return _hydrateEvent(await _getEvent(eventId));
749
+ }
750
+
751
+ // -- markFailed --------------------------------------------------------
752
+
753
+ async function markFailed(input) {
754
+ if (!input || typeof input !== "object") {
755
+ throw new TypeError("pixelEvents.markFailed: input object required");
756
+ }
757
+ var eventId = _uuid(input.event_id, "event_id");
758
+ var reason = _reason(input.reason, "reason");
759
+ var retry = !!input.retry;
760
+ var now = _now();
761
+
762
+ var existing = await _getEvent(eventId);
763
+ if (!existing) {
764
+ var nfErr = new Error("pixelEvents.markFailed: event " + eventId + " not found");
765
+ nfErr.code = "EVENT_NOT_FOUND";
766
+ throw nfErr;
767
+ }
768
+ if (existing.status === "dispatched") {
769
+ var fErr = new Error(
770
+ "pixelEvents.markFailed: refusing failed transition from terminal dispatched"
771
+ );
772
+ fErr.code = "FSM_TERMINAL";
773
+ throw fErr;
774
+ }
775
+ var attempts = Number(existing.attempts || 0) + 1;
776
+ if (retry && attempts < RETRY_BACKOFF_MS.length) {
777
+ // Re-queue with back-off — the dispatcher's next tick won't
778
+ // pick the row until `occurred_at` slides past `now +
779
+ // RETRY_BACKOFF_MS[attempts - 1]`.
780
+ var nextOccurredAt = now + RETRY_BACKOFF_MS[attempts - 1];
781
+ await query(
782
+ "UPDATE pixel_events SET status = 'queued', attempts = ?1, " +
783
+ "occurred_at = ?2, last_failure = ?3 WHERE id = ?4",
784
+ [attempts, nextOccurredAt, reason, eventId],
785
+ );
786
+ return _hydrateEvent(await _getEvent(eventId));
787
+ }
788
+ // Terminal failure — exhausted retry budget OR caller asked for
789
+ // a hard fail. Persist the reason + flip status.
790
+ await query(
791
+ "UPDATE pixel_events SET status = 'failed', attempts = ?1, " +
792
+ "last_failure = ?2 WHERE id = ?3",
793
+ [attempts, reason, eventId],
794
+ );
795
+ return _hydrateEvent(await _getEvent(eventId));
796
+ }
797
+
798
+ // -- eventsForOrder ----------------------------------------------------
799
+
800
+ async function eventsForOrder(orderId) {
801
+ var oid = _uuid(orderId, "order_id");
802
+ var r = await query(
803
+ "SELECT * FROM pixel_events WHERE order_id = ?1 " +
804
+ "ORDER BY occurred_at ASC, id ASC",
805
+ [oid],
806
+ );
807
+ var out = [];
808
+ for (var i = 0; i < r.rows.length; i += 1) out.push(_hydrateEvent(r.rows[i]));
809
+ return out;
810
+ }
811
+
812
+ // -- dispatchedInPeriod ------------------------------------------------
813
+
814
+ async function dispatchedInPeriod(input) {
815
+ if (!input || typeof input !== "object") {
816
+ throw new TypeError("pixelEvents.dispatchedInPeriod: input object required");
817
+ }
818
+ var providerSlug = input.provider_slug == null ? null : _slug(input.provider_slug, "provider_slug");
819
+ var from = _epochMs(input.from, "from");
820
+ var to = _epochMs(input.to, "to");
821
+ var limit = _limit(input.limit, "limit");
822
+ if (from > to) {
823
+ throw new TypeError("pixelEvents.dispatchedInPeriod: from must be <= to");
824
+ }
825
+
826
+ var sql, params;
827
+ if (providerSlug) {
828
+ sql = "SELECT * FROM pixel_events " +
829
+ "WHERE status = 'dispatched' AND provider_slug = ?1 " +
830
+ "AND dispatched_at >= ?2 AND dispatched_at <= ?3 " +
831
+ "ORDER BY dispatched_at ASC, id ASC LIMIT ?4";
832
+ params = [providerSlug, from, to, limit];
833
+ } else {
834
+ sql = "SELECT * FROM pixel_events " +
835
+ "WHERE status = 'dispatched' " +
836
+ "AND dispatched_at >= ?1 AND dispatched_at <= ?2 " +
837
+ "ORDER BY dispatched_at ASC, id ASC LIMIT ?3";
838
+ params = [from, to, limit];
839
+ }
840
+ var r = await query(sql, params);
841
+ var out = [];
842
+ for (var i = 0; i < r.rows.length; i += 1) out.push(_hydrateEvent(r.rows[i]));
843
+ return out;
844
+ }
845
+
846
+ // -- failedEvents ------------------------------------------------------
847
+
848
+ async function failedEvents(input) {
849
+ input = input || {};
850
+ var limit = _limit(input.limit, "limit");
851
+ var r = await query(
852
+ "SELECT * FROM pixel_events WHERE status = 'failed' " +
853
+ "ORDER BY created_at DESC, id ASC LIMIT ?1",
854
+ [limit],
855
+ );
856
+ var out = [];
857
+ for (var i = 0; i < r.rows.length; i += 1) out.push(_hydrateEvent(r.rows[i]));
858
+ return out;
859
+ }
860
+
861
+ // -- metricsForProvider ------------------------------------------------
862
+ //
863
+ // Per-event-name rollup across [from, to]. Computes total / queued
864
+ // / dispatched / failed counts so the operator dashboard can
865
+ // surface dispatch-success ratio per provider per event type.
866
+
867
+ async function metricsForProvider(input) {
868
+ if (!input || typeof input !== "object") {
869
+ throw new TypeError("pixelEvents.metricsForProvider: input object required");
870
+ }
871
+ var slug = _slug(input.slug, "slug");
872
+ var from = _epochMs(input.from, "from");
873
+ var to = _epochMs(input.to, "to");
874
+ if (from > to) {
875
+ throw new TypeError("pixelEvents.metricsForProvider: from must be <= to");
876
+ }
877
+ var provider = await _getProvider(slug);
878
+ if (!provider) {
879
+ throw new TypeError("pixelEvents.metricsForProvider: slug " + JSON.stringify(slug) + " not found");
880
+ }
881
+
882
+ var rows = (await query(
883
+ "SELECT event_name, status, COUNT(*) AS n FROM pixel_events " +
884
+ "WHERE provider_slug = ?1 AND occurred_at >= ?2 AND occurred_at <= ?3 " +
885
+ "GROUP BY event_name, status",
886
+ [slug, from, to],
887
+ )).rows;
888
+
889
+ var byEvent = {};
890
+ var totals = { total: 0, queued: 0, dispatched: 0, failed: 0 };
891
+ for (var i = 0; i < EVENT_NAMES.length; i += 1) {
892
+ byEvent[EVENT_NAMES[i]] = { total: 0, queued: 0, dispatched: 0, failed: 0 };
893
+ }
894
+ for (var j = 0; j < rows.length; j += 1) {
895
+ var row = rows[j];
896
+ var name = row.event_name;
897
+ var status = row.status;
898
+ var n = Number(row.n);
899
+ if (byEvent[name]) {
900
+ byEvent[name].total += n;
901
+ if (byEvent[name][status] != null) byEvent[name][status] += n;
902
+ }
903
+ totals.total += n;
904
+ if (totals[status] != null) totals[status] += n;
905
+ }
906
+
907
+ return {
908
+ slug: slug,
909
+ provider: provider.provider,
910
+ pixel_id: provider.pixel_id,
911
+ from: from,
912
+ to: to,
913
+ total: totals.total,
914
+ queued: totals.queued,
915
+ dispatched: totals.dispatched,
916
+ failed: totals.failed,
917
+ by_event: byEvent,
918
+ };
919
+ }
920
+
921
+ // -- listProviders / getProvider --------------------------------------
922
+
923
+ async function getProvider(slug) {
924
+ _slug(slug, "slug");
925
+ return _hydrateProvider(await _getProvider(slug));
926
+ }
927
+
928
+ async function listProviders(listOpts) {
929
+ listOpts = listOpts || {};
930
+ var limit = _limit(listOpts.limit, "limit");
931
+ var sql, params;
932
+ if (listOpts.active_only) {
933
+ sql = "SELECT * FROM pixel_providers WHERE active = 1 " +
934
+ "ORDER BY created_at ASC, slug ASC LIMIT ?1";
935
+ params = [limit];
936
+ } else {
937
+ sql = "SELECT * FROM pixel_providers " +
938
+ "ORDER BY created_at ASC, slug ASC LIMIT ?1";
939
+ params = [limit];
940
+ }
941
+ var rows = (await query(sql, params)).rows;
942
+ var out = [];
943
+ for (var i = 0; i < rows.length; i += 1) out.push(_hydrateProvider(rows[i]));
944
+ return out;
945
+ }
946
+
947
+ return {
948
+ PROVIDERS: PROVIDERS.slice(),
949
+ EVENT_NAMES: EVENT_NAMES.slice(),
950
+ STATUSES: STATUSES.slice(),
951
+ RETRY_BACKOFF_MS: RETRY_BACKOFF_MS.slice(),
952
+ MAX_SLUG_LEN: MAX_SLUG_LEN,
953
+ MAX_EVENT_ID_LEN: MAX_EVENT_ID_LEN,
954
+ MAX_LIST_LIMIT: MAX_LIST_LIMIT,
955
+ DEFAULT_LIST_LIMIT: DEFAULT_LIST_LIMIT,
956
+ MAX_BATCH_SIZE: MAX_BATCH_SIZE,
957
+ DEFAULT_BATCH_SIZE: DEFAULT_BATCH_SIZE,
958
+
959
+ // Helpers surfaced so callers compose with the same hashing the
960
+ // primitive uses internally — a caller that wants to dedupe a row
961
+ // ahead of recordEvent can pre-compute the digest and observe the
962
+ // value the framework would have persisted.
963
+ hashEmail: _hashEmail,
964
+ hashPhone: _hashPhone,
965
+ normaliseEmail: _normaliseEmail,
966
+ normalisePhone: _normalisePhone,
967
+
968
+ registerProvider: registerProvider,
969
+ getProvider: getProvider,
970
+ listProviders: listProviders,
971
+ recordEvent: recordEvent,
972
+ dispatchTick: dispatchTick,
973
+ markDispatched: markDispatched,
974
+ markFailed: markFailed,
975
+ eventsForOrder: eventsForOrder,
976
+ dispatchedInPeriod: dispatchedInPeriod,
977
+ failedEvents: failedEvents,
978
+ metricsForProvider: metricsForProvider,
979
+ };
980
+ }
981
+
982
+ module.exports = {
983
+ create: create,
984
+ PROVIDERS: PROVIDERS.slice(),
985
+ EVENT_NAMES: EVENT_NAMES.slice(),
986
+ STATUSES: STATUSES.slice(),
987
+ RETRY_BACKOFF_MS: RETRY_BACKOFF_MS.slice(),
988
+ MAX_SLUG_LEN: MAX_SLUG_LEN,
989
+ MAX_EVENT_ID_LEN: MAX_EVENT_ID_LEN,
990
+ MAX_LIST_LIMIT: MAX_LIST_LIMIT,
991
+ DEFAULT_LIST_LIMIT: DEFAULT_LIST_LIMIT,
992
+ MAX_BATCH_SIZE: MAX_BATCH_SIZE,
993
+ DEFAULT_BATCH_SIZE: DEFAULT_BATCH_SIZE,
994
+ SHA256_HEX_RE: SHA256_HEX_RE,
995
+ };