@blamejs/blamejs-shop 0.5.21 → 0.5.23
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 +4 -0
- package/README.md +2 -1
- package/SECURITY.md +25 -1
- package/lib/addresses.js +16 -2
- package/lib/admin.js +116 -1
- package/lib/affiliates.js +3 -3
- package/lib/announcement-bar.js +2 -2
- package/lib/api-keys.js +2 -2
- package/lib/asset-manifest.json +5 -5
- package/lib/blog-articles.js +2 -2
- package/lib/carrier-accounts.js +2 -2
- package/lib/catalog-drafts.js +1 -1
- package/lib/category-navigation.js +6 -3
- package/lib/compliance-export.js +40 -3
- package/lib/customer-impersonation.js +105 -7
- package/lib/customer-notes.js +1 -1
- package/lib/customer-roles.js +1 -1
- package/lib/customer-surveys.js +2 -2
- package/lib/customers.js +1 -1
- package/lib/dispute-resolution.js +3 -3
- package/lib/dropship-forwarding.js +1 -1
- package/lib/dunning.js +1 -1
- package/lib/email-ab-tests.js +1 -1
- package/lib/email-campaigns.js +15 -2
- package/lib/email-warmup.js +1 -1
- package/lib/externaldb-d1.js +0 -8
- package/lib/gift-options.js +3 -3
- package/lib/knowledge-base.js +3 -3
- package/lib/line-gift-wrap.js +6 -2
- package/lib/live-chat.js +1 -1
- package/lib/notifications.js +1 -1
- package/lib/operator-accounts.js +1 -1
- package/lib/operator-help-center.js +3 -3
- package/lib/operator-inbox.js +2 -2
- package/lib/operator-roles.js +1 -1
- package/lib/order-notes.js +2 -2
- package/lib/order-ratings.js +3 -3
- package/lib/payment-methods.js +1 -1
- package/lib/payment-retries.js +1 -1
- package/lib/pixel-events.js +14 -6
- package/lib/plan-changes.js +1 -1
- package/lib/product-qa.js +1 -1
- package/lib/push-notifications.js +2 -2
- package/lib/reviews.js +3 -3
- package/lib/seller-signup.js +4 -4
- package/lib/sidebar-widgets.js +1 -1
- package/lib/sms-dispatcher.js +3 -3
- package/lib/stock-receipts.js +1 -1
- package/lib/storefront-dashboards.js +1 -1
- package/lib/storefront-forms.js +1 -1
- package/lib/storefront-pages.js +2 -2
- package/lib/storefront.js +569 -1
- package/lib/subscription-billing.js +1 -1
- package/lib/subscription-controls.js +1 -1
- package/lib/suggestion-box.js +4 -4
- package/lib/support-tickets.js +3 -3
- package/lib/tenants.js +1 -1
- package/lib/text-guard.js +44 -2
- package/lib/theme-assets.js +1 -1
- package/lib/vendor/MANIFEST.json +12 -12
- package/lib/vendor/blamejs/CHANGELOG.md +32 -0
- package/lib/vendor/blamejs/NOTICE +1 -1
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/lib/network-tls.js +98 -13
- package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +12 -12
- package/lib/vendor/blamejs/lib/vendor/blamejs-pki.cjs +568 -304
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/sbom.cdx.json +6 -6
- package/lib/vendors.js +3 -3
- package/lib/webhook-receiver.js +1 -1
- package/lib/webhook-subscriptions.js +1 -1
- package/lib/wishlist-sharing.js +1 -1
- package/package.json +1 -1
|
@@ -113,8 +113,14 @@
|
|
|
113
113
|
* → `{ notified: boolean, customer_notified_at: <ms> | null }`
|
|
114
114
|
* - `listForOperator(operator_id, { active_only? })`
|
|
115
115
|
* → array of session rows, newest-first.
|
|
116
|
-
* - `listForCustomer(customer_id)`
|
|
117
|
-
* →
|
|
116
|
+
* - `listForCustomer(customer_id, { limit?, offset? })`
|
|
117
|
+
* → the customer's most recent sessions, newest-first. BOUNDED:
|
|
118
|
+
* 20 by default, 200 at most. The storefront renders this on
|
|
119
|
+
* every account page load and the rows are never pruned, so an
|
|
120
|
+
* unbounded read would make a customer's own dashboard slower
|
|
121
|
+
* for the rest of the account's life. Nothing becomes unreadable:
|
|
122
|
+
* `offset` walks the older pages, so a customer or a regulator
|
|
123
|
+
* asking for the whole history can still get it.
|
|
118
124
|
* - `currentlyImpersonating()`
|
|
119
125
|
* → array of active session rows.
|
|
120
126
|
* - `cleanupExpired({ now? })`
|
|
@@ -138,6 +144,13 @@
|
|
|
138
144
|
var TOKEN_NAMESPACE = "customer-impersonation-token";
|
|
139
145
|
var TOKEN_BYTES = 32;
|
|
140
146
|
var DEFAULT_TTL_SECONDS = 60 * 60; // allow:raw-time-literal — seconds value; C.TIME returns ms (60-minute default)
|
|
147
|
+
|
|
148
|
+
// How much of a customer's access history their own account page shows by
|
|
149
|
+
// default, and the ceiling a caller may ask for. Bounded because the rows are
|
|
150
|
+
// an audit trail that is never pruned: an unbounded read would make a
|
|
151
|
+
// customer's dashboard slower every time support helped them.
|
|
152
|
+
var DEFAULT_CUSTOMER_HISTORY = 20;
|
|
153
|
+
var MAX_CUSTOMER_HISTORY = 200;
|
|
141
154
|
var MIN_TTL_SECONDS = 60; // refuse sub-minute
|
|
142
155
|
var MAX_TTL_SECONDS = 8 * 60 * 60; // allow:raw-time-literal — seconds value; C.TIME returns ms (hard ceiling — eight hours)
|
|
143
156
|
var MAX_REASON_LEN = 280;
|
|
@@ -165,6 +178,24 @@ function _uuid(s, label) {
|
|
|
165
178
|
}
|
|
166
179
|
}
|
|
167
180
|
|
|
181
|
+
// The admin console has two kinds of operator identity: a staff account, whose
|
|
182
|
+
// id is a v7 UUID, and the bootstrap / break-glass credential, whose id is the
|
|
183
|
+
// reserved literal below. Both are real operators and both must be recordable
|
|
184
|
+
// here — a store running the single-credential console (the common deploy) has
|
|
185
|
+
// no UUID to offer, and refusing it would make the feature unreachable exactly
|
|
186
|
+
// where the audit trail matters most.
|
|
187
|
+
//
|
|
188
|
+
// Only this one literal is admitted; anything else still has to be a UUID, so
|
|
189
|
+
// the column cannot fill up with free-form operator labels. It matches the id
|
|
190
|
+
// the rest of the console records against the same action, which keeps an
|
|
191
|
+
// impersonation row joinable to its audit entries.
|
|
192
|
+
var BOOTSTRAP_OPERATOR_ID = "owner";
|
|
193
|
+
|
|
194
|
+
function _operatorId(s, label) {
|
|
195
|
+
if (s === BOOTSTRAP_OPERATOR_ID) return s;
|
|
196
|
+
return _uuid(s, label);
|
|
197
|
+
}
|
|
198
|
+
|
|
168
199
|
function _requiredString(s, label, maxLen) {
|
|
169
200
|
if (typeof s !== "string") {
|
|
170
201
|
throw new TypeError("customer-impersonation: " + label + " must be a string");
|
|
@@ -308,7 +339,7 @@ function create(opts) {
|
|
|
308
339
|
if (!input || typeof input !== "object") {
|
|
309
340
|
throw new TypeError("customer-impersonation.startImpersonation: input object required");
|
|
310
341
|
}
|
|
311
|
-
var operatorId =
|
|
342
|
+
var operatorId = _operatorId(input.operator_id, "operator_id");
|
|
312
343
|
var customerId = _uuid(input.customer_id, "customer_id");
|
|
313
344
|
var reason = _requiredString(input.reason, "reason", MAX_REASON_LEN);
|
|
314
345
|
var ttl = _ttlSeconds(input.ttl_seconds);
|
|
@@ -543,7 +574,7 @@ function create(opts) {
|
|
|
543
574
|
// started, newest-first. `active_only: true` filters to live
|
|
544
575
|
// sessions only.
|
|
545
576
|
listForOperator: async function (operatorId, listOpts) {
|
|
546
|
-
var oid =
|
|
577
|
+
var oid = _operatorId(operatorId, "operator_id");
|
|
547
578
|
listOpts = listOpts || {};
|
|
548
579
|
if (typeof listOpts !== "object") {
|
|
549
580
|
throw new TypeError("customer-impersonation.listForOperator: opts must be an object");
|
|
@@ -568,12 +599,33 @@ function create(opts) {
|
|
|
568
599
|
// Returns every impersonation session ever opened against the
|
|
569
600
|
// customer, newest-first. The customer-side "show me who's looked
|
|
570
601
|
// at my account" page reads this directly.
|
|
571
|
-
|
|
602
|
+
// `limit` is bounded and defaulted because the storefront renders this on
|
|
603
|
+
// every /account load. These rows are an audit trail and are never
|
|
604
|
+
// deleted, so an unbounded read would grow the query cost and the page
|
|
605
|
+
// weight of a customer's own dashboard for the rest of the account's life
|
|
606
|
+
// — worst for exactly the customers support has helped most often.
|
|
607
|
+
listForCustomer: async function (customerId, listOpts) {
|
|
572
608
|
var cid = _uuid(customerId, "customer_id");
|
|
609
|
+
var opts = listOpts || {};
|
|
610
|
+
var limit = opts.limit == null ? DEFAULT_CUSTOMER_HISTORY : opts.limit;
|
|
611
|
+
if (typeof limit !== "number" || !Number.isInteger(limit) || limit <= 0 ||
|
|
612
|
+
limit > MAX_CUSTOMER_HISTORY) {
|
|
613
|
+
throw new TypeError("customer-impersonation.listForCustomer: limit must be an integer in [1, " +
|
|
614
|
+
MAX_CUSTOMER_HISTORY + "]");
|
|
615
|
+
}
|
|
616
|
+
// Offset paging, so capping the page does not put older rows out of
|
|
617
|
+
// reach. These are audit records: a bounded default protects the
|
|
618
|
+
// account page from growing heavier with every support visit, but
|
|
619
|
+
// nothing may become unreadable — a customer or a regulator asking for
|
|
620
|
+
// the whole history has to be able to walk it.
|
|
621
|
+
var offset = opts.offset == null ? 0 : opts.offset;
|
|
622
|
+
if (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0) {
|
|
623
|
+
throw new TypeError("customer-impersonation.listForCustomer: offset must be a non-negative integer");
|
|
624
|
+
}
|
|
573
625
|
var r = await query(
|
|
574
626
|
"SELECT * FROM impersonations WHERE customer_id = ?1 " +
|
|
575
|
-
"ORDER BY started_at DESC, id DESC",
|
|
576
|
-
[cid],
|
|
627
|
+
"ORDER BY started_at DESC, id DESC LIMIT ?2 OFFSET ?3",
|
|
628
|
+
[cid, limit, offset],
|
|
577
629
|
);
|
|
578
630
|
return r.rows.map(_projectRow);
|
|
579
631
|
},
|
|
@@ -703,6 +755,52 @@ function create(opts) {
|
|
|
703
755
|
// `endImpersonation` because the audit log distinguishes
|
|
704
756
|
// operator-driven completion from operator-side termination.
|
|
705
757
|
// Idempotent on already-terminal rows.
|
|
758
|
+
// ---- consumeToken -----------------------------------------------------
|
|
759
|
+
//
|
|
760
|
+
// Spend the handoff bearer WITHOUT ending the session.
|
|
761
|
+
//
|
|
762
|
+
// `startImpersonation` mints a single plaintext token and the operator
|
|
763
|
+
// follows it once, out of the console and into the storefront. Verifying
|
|
764
|
+
// it is a read, though, so until the token stops matching, that URL keeps
|
|
765
|
+
// working: it sits in browser history, in a chat message if it was pasted,
|
|
766
|
+
// in a proxy log — and anyone holding it can mint their own cookie for
|
|
767
|
+
// that customer for the rest of the hour. The handoff is meant to be
|
|
768
|
+
// one-time; this is what makes it so.
|
|
769
|
+
//
|
|
770
|
+
// Rotating the hash to a fresh random value is what spends it. The row's
|
|
771
|
+
// authority now lives entirely in the cookie the redemption sealed, which
|
|
772
|
+
// is bound to the operator's browser; the session stays `active` and
|
|
773
|
+
// `end` / `revoke` / the sweep are unaffected. Rotating (rather than
|
|
774
|
+
// nulling) also keeps the NOT NULL UNIQUE column honest.
|
|
775
|
+
//
|
|
776
|
+
// Takes the PLAINTEXT the caller was presented, and spends only that one.
|
|
777
|
+
//
|
|
778
|
+
// The match on the current hash is what makes this single-use under
|
|
779
|
+
// concurrency. Verification is a read, so two requests carrying the same
|
|
780
|
+
// link can both pass it before either writes; if the update only keyed on
|
|
781
|
+
// the row id they would both succeed and both mint a cookie, which is the
|
|
782
|
+
// guarantee this exists to provide, broken. Conditioning the write on the
|
|
783
|
+
// hash it verified means exactly one request can win — the second finds
|
|
784
|
+
// the hash already rotated and gets `consumed: false`.
|
|
785
|
+
//
|
|
786
|
+
// Deliberately NOT idempotent: a caller that cannot claim the token must
|
|
787
|
+
// not be handed a session.
|
|
788
|
+
consumeToken: async function (impersonationId, plaintext) {
|
|
789
|
+
var impId = _uuid(impersonationId, "impersonation_id");
|
|
790
|
+
if (typeof plaintext !== "string" || !plaintext.length) {
|
|
791
|
+
throw new TypeError("customer-impersonation.consumeToken: plaintext token required");
|
|
792
|
+
}
|
|
793
|
+
var presented = b.crypto.namespaceHash(TOKEN_NAMESPACE, plaintext);
|
|
794
|
+
var spent = b.crypto.namespaceHash(
|
|
795
|
+
TOKEN_NAMESPACE, "spent:" + impId + ":" + b.crypto.toBase64Url(b.crypto.generateBytes(32)));
|
|
796
|
+
var r = await query(
|
|
797
|
+
"UPDATE impersonations SET token_hash = ?1 " +
|
|
798
|
+
"WHERE id = ?2 AND status = 'active' AND token_hash = ?3",
|
|
799
|
+
[spent, impId, presented],
|
|
800
|
+
);
|
|
801
|
+
return { consumed: r.rowCount === 1 };
|
|
802
|
+
},
|
|
803
|
+
|
|
706
804
|
revoke: async function (input) {
|
|
707
805
|
if (!input || typeof input !== "object") {
|
|
708
806
|
throw new TypeError("customer-impersonation.revoke: input object required");
|
package/lib/customer-notes.js
CHANGED
|
@@ -142,7 +142,7 @@ function _body(s) {
|
|
|
142
142
|
if (s.length > MAX_BODY_LEN) {
|
|
143
143
|
throw new TypeError("customerNotes: body must be <= " + MAX_BODY_LEN + " characters");
|
|
144
144
|
}
|
|
145
|
-
textGuard.freeText(s, "customerNotes: body", { zeroWidth: "reject" });
|
|
145
|
+
textGuard.freeText(s, "customerNotes: body", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
146
146
|
return s;
|
|
147
147
|
}
|
|
148
148
|
|
package/lib/customer-roles.js
CHANGED
|
@@ -152,7 +152,7 @@ function _title(s) {
|
|
|
152
152
|
if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
|
|
153
153
|
throw new TypeError("customerRoles: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
|
|
154
154
|
}
|
|
155
|
-
textGuard.freeText(s, "customerRoles: title", { singleLine: "reject" });
|
|
155
|
+
textGuard.freeText(s, "customerRoles: title", { singleLine: "reject", bidiMarks: "allow" });
|
|
156
156
|
return s;
|
|
157
157
|
}
|
|
158
158
|
|
package/lib/customer-surveys.js
CHANGED
|
@@ -142,7 +142,7 @@ function _title(s) {
|
|
|
142
142
|
if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
|
|
143
143
|
throw new TypeError("customerSurveys: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
|
|
144
144
|
}
|
|
145
|
-
textGuard.freeText(s, "customerSurveys: title");
|
|
145
|
+
textGuard.freeText(s, "customerSurveys: title", { bidiMarks: "allow" });
|
|
146
146
|
return s;
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -187,7 +187,7 @@ function _closeReason(s) {
|
|
|
187
187
|
if (typeof s !== "string" || !s.length || s.length > MAX_CLOSE_REASON_LEN) {
|
|
188
188
|
throw new TypeError("customerSurveys: reason must be a non-empty string <= " + MAX_CLOSE_REASON_LEN + " chars");
|
|
189
189
|
}
|
|
190
|
-
textGuard.freeText(s, "customerSurveys: reason");
|
|
190
|
+
textGuard.freeText(s, "customerSurveys: reason", { bidiMarks: "allow" });
|
|
191
191
|
return s;
|
|
192
192
|
}
|
|
193
193
|
|
package/lib/customers.js
CHANGED
|
@@ -105,7 +105,7 @@ function _displayName(s) {
|
|
|
105
105
|
if (s.length > MAX_DISPLAY_NAME_LEN) {
|
|
106
106
|
throw new TypeError("customers: display_name must be <= " + MAX_DISPLAY_NAME_LEN + " characters");
|
|
107
107
|
}
|
|
108
|
-
textGuard.freeText(s, "customers: display_name", { singleLine: "reject" });
|
|
108
|
+
textGuard.freeText(s, "customers: display_name", { singleLine: "reject", bidiMarks: "allow" });
|
|
109
109
|
return s;
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -249,7 +249,7 @@ function _narrative(s) {
|
|
|
249
249
|
" exceeds cap " + MAX_NARRATIVE_LEN
|
|
250
250
|
);
|
|
251
251
|
}
|
|
252
|
-
textGuard.freeText(s, "disputeResolution: narrative");
|
|
252
|
+
textGuard.freeText(s, "disputeResolution: narrative", { bidiMarks: "allow" });
|
|
253
253
|
return s;
|
|
254
254
|
}
|
|
255
255
|
|
|
@@ -277,7 +277,7 @@ function _notesOpt(s) {
|
|
|
277
277
|
"disputeResolution: notes length " + s.length + " exceeds cap " + MAX_NOTES_LEN
|
|
278
278
|
);
|
|
279
279
|
}
|
|
280
|
-
textGuard.freeText(s, "disputeResolution: notes");
|
|
280
|
+
textGuard.freeText(s, "disputeResolution: notes", { bidiMarks: "allow" });
|
|
281
281
|
return s;
|
|
282
282
|
}
|
|
283
283
|
|
|
@@ -301,7 +301,7 @@ function _writeoffReason(s) {
|
|
|
301
301
|
" exceeds cap " + MAX_WRITEOFF_REASON_LEN
|
|
302
302
|
);
|
|
303
303
|
}
|
|
304
|
-
textGuard.freeText(s, "disputeResolution: writeoff reason");
|
|
304
|
+
textGuard.freeText(s, "disputeResolution: writeoff reason", { bidiMarks: "allow" });
|
|
305
305
|
return s;
|
|
306
306
|
}
|
|
307
307
|
|
|
@@ -183,7 +183,7 @@ function _returnPolicy(s) {
|
|
|
183
183
|
if (typeof s !== "string" || !s.length || s.length > MAX_RETURN_POLICY_LEN) {
|
|
184
184
|
throw new TypeError("dropship-forwarding: return_policy must be a non-empty string <= " + MAX_RETURN_POLICY_LEN + " chars");
|
|
185
185
|
}
|
|
186
|
-
textGuard.freeText(s, "dropship-forwarding: return_policy", { singleLine: "reject" });
|
|
186
|
+
textGuard.freeText(s, "dropship-forwarding: return_policy", { singleLine: "reject", bidiMarks: "allow" });
|
|
187
187
|
}
|
|
188
188
|
function _positiveInt(n, label) {
|
|
189
189
|
if (!Number.isInteger(n) || n <= 0) {
|
package/lib/dunning.js
CHANGED
|
@@ -202,7 +202,7 @@ function _reason(s) {
|
|
|
202
202
|
if (s.length > MAX_REASON_LEN) {
|
|
203
203
|
throw new TypeError("dunning: reason must be <= " + MAX_REASON_LEN + " characters");
|
|
204
204
|
}
|
|
205
|
-
textGuard.freeText(s, "dunning: reason", { zeroWidth: "reject" });
|
|
205
|
+
textGuard.freeText(s, "dunning: reason", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
206
206
|
return s;
|
|
207
207
|
}
|
|
208
208
|
|
package/lib/email-ab-tests.js
CHANGED
|
@@ -165,7 +165,7 @@ function _title(s) {
|
|
|
165
165
|
if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
|
|
166
166
|
throw new TypeError("emailABTests: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
|
|
167
167
|
}
|
|
168
|
-
textGuard.freeText(s, "emailABTests: title");
|
|
168
|
+
textGuard.freeText(s, "emailABTests: title", { bidiMarks: "allow" });
|
|
169
169
|
return s;
|
|
170
170
|
}
|
|
171
171
|
|
package/lib/email-campaigns.js
CHANGED
|
@@ -170,12 +170,25 @@ var SEND_OUTCOMES = ["sent", "failed", "skipped_unsubscribed", "skipped_suppress
|
|
|
170
170
|
// policy rather than a local class. The class this replaced covered the
|
|
171
171
|
// zero-width family but not the bidi overrides, which let a campaign body
|
|
172
172
|
// carry a URL that displays as one host and resolves to another.
|
|
173
|
-
|
|
173
|
+
// `bidiMarks` too: prose legitimately carries a direction mark (an Arabic
|
|
174
|
+
// street name, a Hebrew review) and the shared screen permits them by default,
|
|
175
|
+
// but a URL is machine-plain — a mark inside one is only ever there to make the
|
|
176
|
+
// host read as something it is not.
|
|
177
|
+
var LINK_URL_POLICY = Object.freeze({
|
|
178
|
+
singleLine: "reject",
|
|
179
|
+
zeroWidth: "reject",
|
|
180
|
+
bidiMarks: "reject",
|
|
181
|
+
});
|
|
174
182
|
|
|
175
183
|
// Free-form text folded into one ledger line. Stripped rather than refused —
|
|
176
184
|
// this runs on an error path that must not itself fail.
|
|
177
185
|
var CONTROL_BYTE_LINE_RE = /[\u0000-\u001f\u007f]/;
|
|
178
186
|
|
|
187
|
+
// Global twin, derived so the class is written once. `.replace` with an
|
|
188
|
+
// unglobbed regex rewrites only the FIRST match, and a mailer's error message
|
|
189
|
+
// can carry several control bytes — the rest would reach the stored ledger row.
|
|
190
|
+
var CONTROL_BYTE_LINE_RE_G = new RegExp(CONTROL_BYTE_LINE_RE.source, "g");
|
|
191
|
+
|
|
179
192
|
var STATUSES = ["draft", "scheduled", "sending", "sent", "paused", "cancelled"];
|
|
180
193
|
var EVENT_TYPES = ["delivered", "opened", "clicked", "bounced", "unsubscribed"];
|
|
181
194
|
var TERMINAL = ["sent", "cancelled"];
|
|
@@ -982,7 +995,7 @@ function create(opts) {
|
|
|
982
995
|
// strip control bytes (no CRLF / NUL into the stored row) and
|
|
983
996
|
// cap length. The recipient address is never echoed into the
|
|
984
997
|
// reason — only the mailer's own message.
|
|
985
|
-
var reason = String(sendErr && sendErr.message || sendErr).replace(
|
|
998
|
+
var reason = String(sendErr && sendErr.message || sendErr).replace(CONTROL_BYTE_LINE_RE_G, " ").slice(0, 280);
|
|
986
999
|
await _finalizeSend(row.slug, consentHash || emailHash, "failed", reason);
|
|
987
1000
|
continue;
|
|
988
1001
|
}
|
package/lib/email-warmup.js
CHANGED
|
@@ -231,7 +231,7 @@ function _reason(s) {
|
|
|
231
231
|
if (s.length > MAX_REASON_LEN) {
|
|
232
232
|
throw new TypeError("emailWarmup: reason must be <= " + MAX_REASON_LEN + " characters");
|
|
233
233
|
}
|
|
234
|
-
textGuard.freeText(s, "emailWarmup: reason", { singleLine: "reject", zeroWidth: "reject" });
|
|
234
|
+
textGuard.freeText(s, "emailWarmup: reason", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
|
|
235
235
|
return s;
|
|
236
236
|
}
|
|
237
237
|
|
package/lib/externaldb-d1.js
CHANGED
|
@@ -181,14 +181,6 @@ function _isTransient(err) {
|
|
|
181
181
|
return c === "ECONNRESET" || c === "ETIMEDOUT" || c === "ENOTFOUND" || c === "EAI_AGAIN" || c === "UND_ERR_SOCKET";
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
function _backoff(attempt) {
|
|
185
|
-
// Jittered exponential — 200ms, 400ms, ... with ±25% jitter so
|
|
186
|
-
// retries from many container replicas don't synchronize.
|
|
187
|
-
var base = RETRY_BACKOFF_MS * Math.pow(2, attempt);
|
|
188
|
-
var jitter = base * (0.75 + (b.crypto.randomInt(0, 50) / 100));
|
|
189
|
-
return new Promise(function (r) { setTimeout(r, jitter); });
|
|
190
|
-
}
|
|
191
|
-
|
|
192
184
|
function _httpErr(status, label) {
|
|
193
185
|
var e = new Error("externaldbD1: " + label + " failed with HTTP " + status);
|
|
194
186
|
e.code = "D1_HTTP_" + status;
|
package/lib/gift-options.js
CHANGED
|
@@ -103,7 +103,7 @@ function _title(s) {
|
|
|
103
103
|
if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
|
|
104
104
|
throw new TypeError("giftOptions: title must be a non-empty string ≤ " + MAX_TITLE_LEN + " chars");
|
|
105
105
|
}
|
|
106
|
-
textGuard.freeText(s, "giftOptions: title", { singleLine: "reject", zeroWidth: "reject" });
|
|
106
|
+
textGuard.freeText(s, "giftOptions: title", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
|
|
107
107
|
return s;
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -154,7 +154,7 @@ function _giftMessage(s) {
|
|
|
154
154
|
if (s.length > MAX_MESSAGE_LEN) {
|
|
155
155
|
throw new TypeError("giftOptions: gift_message must be ≤ " + MAX_MESSAGE_LEN + " chars");
|
|
156
156
|
}
|
|
157
|
-
textGuard.freeText(s, "giftOptions: gift_message", { zeroWidth: "reject" });
|
|
157
|
+
textGuard.freeText(s, "giftOptions: gift_message", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
158
158
|
return s;
|
|
159
159
|
}
|
|
160
160
|
|
|
@@ -166,7 +166,7 @@ function _recipientName(s) {
|
|
|
166
166
|
if (s.length > MAX_RECIPIENT_LEN) {
|
|
167
167
|
throw new TypeError("giftOptions: recipient_name must be ≤ " + MAX_RECIPIENT_LEN + " chars");
|
|
168
168
|
}
|
|
169
|
-
textGuard.freeText(s, "giftOptions: recipient_name", { singleLine: "reject", zeroWidth: "reject" });
|
|
169
|
+
textGuard.freeText(s, "giftOptions: recipient_name", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
|
|
170
170
|
return s;
|
|
171
171
|
}
|
|
172
172
|
|
package/lib/knowledge-base.js
CHANGED
|
@@ -159,7 +159,7 @@ function _title(s) {
|
|
|
159
159
|
if (s.length > MAX_TITLE_LEN) {
|
|
160
160
|
throw new TypeError("knowledgeBase: title must be <= " + MAX_TITLE_LEN + " characters");
|
|
161
161
|
}
|
|
162
|
-
textGuard.freeText(s, "knowledgeBase: title", { singleLine: "reject", zeroWidth: "reject" });
|
|
162
|
+
textGuard.freeText(s, "knowledgeBase: title", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
|
|
163
163
|
return s;
|
|
164
164
|
}
|
|
165
165
|
|
|
@@ -177,7 +177,7 @@ function _body(s) {
|
|
|
177
177
|
if (textGuard.hasCodepointThreat(s)) {
|
|
178
178
|
throw new TypeError("knowledgeBase: body contains control bytes");
|
|
179
179
|
}
|
|
180
|
-
textGuard.freeText(s, "knowledgeBase: body", { zeroWidth: "reject" });
|
|
180
|
+
textGuard.freeText(s, "knowledgeBase: body", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
181
181
|
return s;
|
|
182
182
|
}
|
|
183
183
|
|
|
@@ -271,7 +271,7 @@ function _queryStr(s) {
|
|
|
271
271
|
if (s.length > MAX_QUERY_LEN) {
|
|
272
272
|
throw new TypeError("knowledgeBase: query must be <= " + MAX_QUERY_LEN + " characters");
|
|
273
273
|
}
|
|
274
|
-
textGuard.freeText(s, "knowledgeBase: query", { zeroWidth: "reject" });
|
|
274
|
+
textGuard.freeText(s, "knowledgeBase: query", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
275
275
|
return s;
|
|
276
276
|
}
|
|
277
277
|
|
package/lib/line-gift-wrap.js
CHANGED
|
@@ -151,7 +151,10 @@ function _giftMessage(s) {
|
|
|
151
151
|
if (s.length > MAX_MESSAGE_LEN) {
|
|
152
152
|
throw new TypeError("lineGiftWrap: gift_message must be ≤ " + MAX_MESSAGE_LEN + " chars");
|
|
153
153
|
}
|
|
154
|
-
|
|
154
|
+
// Prose a customer writes for another person to read — a message in Arabic
|
|
155
|
+
// or Hebrew carries a direction mark wherever a Latin word or number sits
|
|
156
|
+
// inside the line. The overrides that reverse a run stay refused.
|
|
157
|
+
textGuard.freeText(s, "lineGiftWrap: gift_message", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
155
158
|
return s;
|
|
156
159
|
}
|
|
157
160
|
|
|
@@ -163,7 +166,8 @@ function _recipientName(s) {
|
|
|
163
166
|
if (s.length > MAX_RECIPIENT_LEN) {
|
|
164
167
|
throw new TypeError("lineGiftWrap: recipient_name must be ≤ " + MAX_RECIPIENT_LEN + " chars");
|
|
165
168
|
}
|
|
166
|
-
|
|
169
|
+
// A person's name, in whatever script they write it.
|
|
170
|
+
textGuard.freeText(s, "lineGiftWrap: recipient_name", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
|
|
167
171
|
return s;
|
|
168
172
|
}
|
|
169
173
|
|
package/lib/live-chat.js
CHANGED
|
@@ -130,7 +130,7 @@ function _body(s) {
|
|
|
130
130
|
if (s.length > MAX_BODY_LEN) {
|
|
131
131
|
throw new TypeError("live-chat: body must be <= " + MAX_BODY_LEN + " characters");
|
|
132
132
|
}
|
|
133
|
-
textGuard.freeText(s, "live-chat: body", { zeroWidth: "reject" });
|
|
133
|
+
textGuard.freeText(s, "live-chat: body", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
134
134
|
return s;
|
|
135
135
|
}
|
|
136
136
|
|
package/lib/notifications.js
CHANGED
|
@@ -99,7 +99,7 @@ function _title(s) {
|
|
|
99
99
|
if (s.length > MAX_TITLE_LEN) {
|
|
100
100
|
throw new TypeError("notifications: title must be <= " + MAX_TITLE_LEN + " characters");
|
|
101
101
|
}
|
|
102
|
-
textGuard.freeText(s, "notifications: title");
|
|
102
|
+
textGuard.freeText(s, "notifications: title", { bidiMarks: "allow" });
|
|
103
103
|
return s;
|
|
104
104
|
}
|
|
105
105
|
|
package/lib/operator-accounts.js
CHANGED
|
@@ -212,7 +212,7 @@ function _displayName(s) {
|
|
|
212
212
|
if (typeof s !== "string" || !s.length || s.length > MAX_DISPLAY_NAME_LEN) {
|
|
213
213
|
throw new TypeError("operatorAccounts: display_name must be a non-empty string (<= " + MAX_DISPLAY_NAME_LEN + " chars)");
|
|
214
214
|
}
|
|
215
|
-
textGuard.freeText(s, "operatorAccounts: display_name", { singleLine: "reject" });
|
|
215
|
+
textGuard.freeText(s, "operatorAccounts: display_name", { singleLine: "reject", bidiMarks: "allow" });
|
|
216
216
|
return s;
|
|
217
217
|
}
|
|
218
218
|
|
|
@@ -164,7 +164,7 @@ function _title(s) {
|
|
|
164
164
|
if (s.length > MAX_TITLE_LEN) {
|
|
165
165
|
throw new TypeError("operatorHelpCenter: title must be <= " + MAX_TITLE_LEN + " characters");
|
|
166
166
|
}
|
|
167
|
-
textGuard.freeText(s, "operatorHelpCenter: title", { singleLine: "reject", zeroWidth: "reject" });
|
|
167
|
+
textGuard.freeText(s, "operatorHelpCenter: title", { singleLine: "reject", zeroWidth: "reject", bidiMarks: "allow" });
|
|
168
168
|
return s;
|
|
169
169
|
}
|
|
170
170
|
|
|
@@ -179,7 +179,7 @@ function _body(s) {
|
|
|
179
179
|
if (s.length > MAX_BODY_LEN) {
|
|
180
180
|
throw new TypeError("operatorHelpCenter: body must be <= " + MAX_BODY_LEN + " characters");
|
|
181
181
|
}
|
|
182
|
-
textGuard.freeText(s, "operatorHelpCenter: body", { zeroWidth: "reject" });
|
|
182
|
+
textGuard.freeText(s, "operatorHelpCenter: body", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
183
183
|
return s;
|
|
184
184
|
}
|
|
185
185
|
|
|
@@ -290,7 +290,7 @@ function _queryStr(s) {
|
|
|
290
290
|
if (s.length > MAX_QUERY_LEN) {
|
|
291
291
|
throw new TypeError("operatorHelpCenter: query must be <= " + MAX_QUERY_LEN + " characters");
|
|
292
292
|
}
|
|
293
|
-
textGuard.freeText(s, "operatorHelpCenter: query", { zeroWidth: "reject" });
|
|
293
|
+
textGuard.freeText(s, "operatorHelpCenter: query", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
294
294
|
return s;
|
|
295
295
|
}
|
|
296
296
|
|
package/lib/operator-inbox.js
CHANGED
|
@@ -248,7 +248,7 @@ function _subject(s) {
|
|
|
248
248
|
if (typeof s !== "string" || !s.length || s.length > MAX_SUBJECT_LEN) {
|
|
249
249
|
throw new TypeError("operatorInbox: subject must be a non-empty string <= " + MAX_SUBJECT_LEN + " chars");
|
|
250
250
|
}
|
|
251
|
-
textGuard.freeText(s, "operatorInbox: subject");
|
|
251
|
+
textGuard.freeText(s, "operatorInbox: subject", { bidiMarks: "allow" });
|
|
252
252
|
return s;
|
|
253
253
|
}
|
|
254
254
|
|
|
@@ -256,7 +256,7 @@ function _body(s) {
|
|
|
256
256
|
if (typeof s !== "string" || !s.length || s.length > MAX_BODY_LEN) {
|
|
257
257
|
throw new TypeError("operatorInbox: body must be a non-empty string <= " + MAX_BODY_LEN + " chars");
|
|
258
258
|
}
|
|
259
|
-
textGuard.freeText(s, "operatorInbox: body");
|
|
259
|
+
textGuard.freeText(s, "operatorInbox: body", { bidiMarks: "allow" });
|
|
260
260
|
return s;
|
|
261
261
|
}
|
|
262
262
|
|
package/lib/operator-roles.js
CHANGED
|
@@ -185,7 +185,7 @@ function _title(s) {
|
|
|
185
185
|
if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
|
|
186
186
|
throw new TypeError("operatorRoles: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
|
|
187
187
|
}
|
|
188
|
-
textGuard.freeText(s, "operatorRoles: title", { singleLine: "reject" });
|
|
188
|
+
textGuard.freeText(s, "operatorRoles: title", { singleLine: "reject", bidiMarks: "allow" });
|
|
189
189
|
return s;
|
|
190
190
|
}
|
|
191
191
|
|
package/lib/order-notes.js
CHANGED
|
@@ -95,7 +95,7 @@ function _body(s) {
|
|
|
95
95
|
if (s.length > MAX_BODY_LEN) {
|
|
96
96
|
throw new TypeError("orderNotes: body must be <= " + MAX_BODY_LEN + " characters");
|
|
97
97
|
}
|
|
98
|
-
textGuard.freeText(s, "orderNotes: body", { zeroWidth: "reject" });
|
|
98
|
+
textGuard.freeText(s, "orderNotes: body", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
99
99
|
return s;
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -131,7 +131,7 @@ function _resolution(s) {
|
|
|
131
131
|
if (s.length > MAX_RESOLUTION_LEN) {
|
|
132
132
|
throw new TypeError("orderNotes: resolution must be <= " + MAX_RESOLUTION_LEN + " characters");
|
|
133
133
|
}
|
|
134
|
-
textGuard.freeText(s, "orderNotes: resolution", { zeroWidth: "reject" });
|
|
134
|
+
textGuard.freeText(s, "orderNotes: resolution", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
135
135
|
return s;
|
|
136
136
|
}
|
|
137
137
|
|
package/lib/order-ratings.js
CHANGED
|
@@ -130,7 +130,7 @@ function _comment(s) {
|
|
|
130
130
|
if (s.length > MAX_COMMENT_LEN) {
|
|
131
131
|
throw new TypeError("orderRatings: comment must be <= " + MAX_COMMENT_LEN + " chars");
|
|
132
132
|
}
|
|
133
|
-
textGuard.freeText(s, "orderRatings: comment");
|
|
133
|
+
textGuard.freeText(s, "orderRatings: comment", { bidiMarks: "allow" });
|
|
134
134
|
return s;
|
|
135
135
|
}
|
|
136
136
|
|
|
@@ -138,7 +138,7 @@ function _flagReason(s) {
|
|
|
138
138
|
if (typeof s !== "string" || !s.length || s.length > MAX_FLAG_REASON_LEN) {
|
|
139
139
|
throw new TypeError("orderRatings: reason must be a non-empty string <= " + MAX_FLAG_REASON_LEN + " chars");
|
|
140
140
|
}
|
|
141
|
-
textGuard.freeText(s, "orderRatings: reason");
|
|
141
|
+
textGuard.freeText(s, "orderRatings: reason", { bidiMarks: "allow" });
|
|
142
142
|
return s;
|
|
143
143
|
}
|
|
144
144
|
|
|
@@ -146,7 +146,7 @@ function _responseText(s) {
|
|
|
146
146
|
if (typeof s !== "string" || !s.length || s.length > MAX_RESPONSE_LEN) {
|
|
147
147
|
throw new TypeError("orderRatings: response must be a non-empty string <= " + MAX_RESPONSE_LEN + " chars");
|
|
148
148
|
}
|
|
149
|
-
textGuard.freeText(s, "orderRatings: response");
|
|
149
|
+
textGuard.freeText(s, "orderRatings: response", { bidiMarks: "allow" });
|
|
150
150
|
return s;
|
|
151
151
|
}
|
|
152
152
|
|
package/lib/payment-methods.js
CHANGED
|
@@ -140,7 +140,7 @@ function _label(s) {
|
|
|
140
140
|
if (s.length > MAX_LABEL_LEN) {
|
|
141
141
|
throw new TypeError("paymentMethods: label must be <= " + MAX_LABEL_LEN + " characters");
|
|
142
142
|
}
|
|
143
|
-
textGuard.freeText(s, "paymentMethods: label", { singleLine: "reject" });
|
|
143
|
+
textGuard.freeText(s, "paymentMethods: label", { singleLine: "reject", bidiMarks: "allow" });
|
|
144
144
|
return s;
|
|
145
145
|
}
|
|
146
146
|
|
package/lib/payment-retries.js
CHANGED
|
@@ -229,7 +229,7 @@ function _reason(s) {
|
|
|
229
229
|
if (s.length > MAX_REASON_LEN) {
|
|
230
230
|
throw new TypeError("paymentRetries: reason must be <= " + MAX_REASON_LEN + " characters");
|
|
231
231
|
}
|
|
232
|
-
textGuard.freeText(s, "paymentRetries: reason", { zeroWidth: "reject" });
|
|
232
|
+
textGuard.freeText(s, "paymentRetries: reason", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
233
233
|
return s;
|
|
234
234
|
}
|
|
235
235
|
|
package/lib/pixel-events.js
CHANGED
|
@@ -403,12 +403,20 @@ function _responseBody(s) {
|
|
|
403
403
|
if (s.length > MAX_RESPONSE_BODY_LEN) {
|
|
404
404
|
throw new TypeError("pixelEvents: response_body must be <= " + MAX_RESPONSE_BODY_LEN + " characters");
|
|
405
405
|
}
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
406
|
+
// SCRUBBED, never refused. This string is whatever an upstream provider sent
|
|
407
|
+
// back, and `markDispatched` runs this validator BEFORE it transitions the
|
|
408
|
+
// event — so any refusal here leaves the row in `queued` and the scheduler
|
|
409
|
+
// re-sends the identical pixel on every tick, forever, because we disliked a
|
|
410
|
+
// byte in the reply. A provider must never be able to hold an event open by
|
|
411
|
+
// choosing its response body.
|
|
412
|
+
//
|
|
413
|
+
// The interest in this field is only that an operator reading the audit
|
|
414
|
+
// trail is not shown invisible characters. Removing them satisfies that
|
|
415
|
+
// without ever failing: the bidi overrides, the direction marks, the
|
|
416
|
+
// zero-width family and the Unicode Tags block all come out, and everything
|
|
417
|
+
// legible — including the tabs and newlines a real response is full of —
|
|
418
|
+
// stays exactly as the provider sent it.
|
|
419
|
+
return textGuard.scrubInvisible(s);
|
|
412
420
|
}
|
|
413
421
|
|
|
414
422
|
function _responseStatus(n) {
|
package/lib/plan-changes.js
CHANGED
|
@@ -115,7 +115,7 @@ function _optReason(s) {
|
|
|
115
115
|
if (s.length > MAX_REASON_LEN) {
|
|
116
116
|
throw new TypeError("planChanges: reason must be <= " + MAX_REASON_LEN + " characters");
|
|
117
117
|
}
|
|
118
|
-
textGuard.freeText(s, "planChanges: reason", { zeroWidth: "reject" });
|
|
118
|
+
textGuard.freeText(s, "planChanges: reason", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
119
119
|
return s;
|
|
120
120
|
}
|
|
121
121
|
|
package/lib/product-qa.js
CHANGED
|
@@ -131,7 +131,7 @@ function _reason(s) {
|
|
|
131
131
|
if (s.length > MAX_REASON_LEN) {
|
|
132
132
|
throw new TypeError("productQA: reason must be <= " + MAX_REASON_LEN + " characters");
|
|
133
133
|
}
|
|
134
|
-
textGuard.freeText(s, "productQA: reason");
|
|
134
|
+
textGuard.freeText(s, "productQA: reason", { bidiMarks: "allow" });
|
|
135
135
|
return s;
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -195,7 +195,7 @@ function _title(s) {
|
|
|
195
195
|
if (s.length > MAX_TITLE_LEN) {
|
|
196
196
|
throw new TypeError("pushNotifications: title must be <= " + MAX_TITLE_LEN + " characters");
|
|
197
197
|
}
|
|
198
|
-
textGuard.freeText(s, "pushNotifications: title", { zeroWidth: "reject" });
|
|
198
|
+
textGuard.freeText(s, "pushNotifications: title", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
199
199
|
return s;
|
|
200
200
|
}
|
|
201
201
|
|
|
@@ -206,7 +206,7 @@ function _body(s) {
|
|
|
206
206
|
if (s.length > MAX_BODY_LEN) {
|
|
207
207
|
throw new TypeError("pushNotifications: body must be <= " + MAX_BODY_LEN + " characters");
|
|
208
208
|
}
|
|
209
|
-
textGuard.freeText(s, "pushNotifications: body", { zeroWidth: "reject" });
|
|
209
|
+
textGuard.freeText(s, "pushNotifications: body", { zeroWidth: "reject", bidiMarks: "allow" });
|
|
210
210
|
return s;
|
|
211
211
|
}
|
|
212
212
|
|