@blamejs/blamejs-shop 0.5.10 → 0.5.11
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 +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/customer-import.js +31 -8
- package/lib/product-import.js +22 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.5.x
|
|
10
10
|
|
|
11
|
+
- v0.5.11 (2026-07-03) — **A second customer or product import started while one is already running is refused instead of clobbering the first run's cancel handle.** Fixes a small concurrency defect in the customer and product CSV/row import runners. Each runner tracks a single in-flight run so the operator console's cancel action knows which run to stop. The run opener set that handle after writing the run row, so two runs started at nearly the same moment — an operator double-submitting the form or opening two tabs — could both open, and the second would overwrite the first's in-flight handle. A subsequent cancel would then target the wrong run, leaving the other running. The opener now reserves the single in-flight slot synchronously, before any await, so a second run is refused with a clear "an import is already in progress" error while the first proceeds; the reservation is released if the run fails to open, and the console can start a fresh import as soon as the running one finishes. **Fixed:** *Overlapping import runs no longer clobber the in-flight run handle* — The customer and product import runners keep one in-flight run handle so cancel targets the right run. Because the opener set that handle only after inserting the run row, two imports started concurrently could both open and the second would overwrite the first's handle, so a later cancel stopped the wrong run. The opener now claims the in-flight slot synchronously (before the first await, so the check-and-set can't interleave) and refuses a second concurrent run with an "import already in progress" error; the slot is released if the open fails, and a new import can start once the running one completes.
|
|
12
|
+
|
|
11
13
|
- v0.5.10 (2026-07-03) — **Redirect rules with a regex source are screened for nested-quantifier catastrophic backtracking before they run on the request path.** Closes the last catastrophic-backtracking (ReDoS) gap in operator-authored redirect rules. A site-redirect of kind "regex" compiles the operator's source into a pattern that resolveForPath runs against the request path on every request. The define-time screen already refused backreferences and lookahead/lookbehind, but not a nested unbounded quantifier — a group whose body carries an unbounded quantifier that is itself repeated by one, such as (a+)+, (a*)*, (?:a+)+ or (.+)+ — so an operator could register a rule whose match time is super-linear in the path length, turning every matching request into a denial of service. The screen now also refuses that shape at define time, at any group depth, while continuing to accept linear patterns that merely use a quantified non-capturing group ((?:/page/[0-9]+)?, (?:section)*) or an optional quantified group. The match is exact — only a nested UNBOUNDED quantifier is refused; a single quantifier, an optional group, and bounded repetition are all allowed — so no well-formed redirect regex that worked before is rejected. **Security:** *Redirect regex sources refuse nested unbounded quantifiers at define time* — A redirect rule of kind "regex" runs its compiled pattern against the request path in resolveForPath. The define-time screen rejected backreferences and lookaround but not a nested unbounded quantifier ((a+)+, (a*)*, (?:a+)+, (.+)+, ((ab)+)+ at any depth) — the canonical ReDoS shape — so an operator could land a rule whose match is super-linear in the path length, letting a crafted request path stall the redirect resolver. The screen now refuses that shape when the rule is defined, with an error pointing the operator at a prefix/exact rule or splitting into multiple rows. Linear patterns are unaffected: a quantified non-capturing group, an optional quantified group, a single quantifier, and bounded repetition are all still accepted, so no redirect regex that worked before is rejected.
|
|
12
14
|
|
|
13
15
|
- v0.5.9 (2026-07-03) — **A federated sign-in no longer auto-merges into a pre-existing account whose email was never proven, closing an account pre-hijacking path.** Closes an account pre-hijacking weakness in federated (Google / Apple) sign-in. When a verified OIDC sign-in found a pre-existing account with the same email, it linked the federated identity into that account on the strength of the incoming email being verified alone — without requiring the pre-existing account to have ever proven control of that email. Because a passkey (or otherwise passwordless) account is created with whatever email address was typed at registration and no ownership check, an attacker could register an account under a victim's email before the victim's first federated sign-in, and the victim's later Google/Apple sign-in would then be folded into the attacker's account — pulling the victim's guest orders in and leaving the attacker's credential enrolled. The merge now requires the pre-existing account to already carry a verified federated identity (the only account-creation path that proves the email); an account whose email was never proven falls through to the existing duplicate-email refusal, so the customer signs in with their existing method and links the provider from account settings. A first federated sign-in still creates a fresh account, and a second provider still links to an account whose email is already proven, so legitimate multi-provider sign-in is unchanged. **Security:** *Federated sign-in refuses to merge into an unverified pre-existing account* — A verified Google/Apple sign-in that matched a pre-existing account by email hash previously linked the federated identity into that account whenever the incoming identity-provider email was verified — even if the pre-existing account had never proven control of the email. A passkey/passwordless account stores the email typed at registration with no verification, so an attacker could pre-seed an account under a victim's email and capture the victim's later federated sign-in (account pre-hijacking, CWE-287). The merge now proceeds only when the pre-existing account already holds a verified federated identity — the one account-creation path that proves the email. An unverified match is refused with the existing duplicate-email conflict, directing the customer to sign in with their existing method and link the provider from account settings. First-time federated sign-in (new account) and second-provider linking into an already-proven account are unchanged.
|
package/lib/asset-manifest.json
CHANGED
package/lib/customer-import.js
CHANGED
|
@@ -182,16 +182,32 @@ function _addError(errors, maxErrors, rowIndex, message) {
|
|
|
182
182
|
// ---- run-row persistence -----------------------------------------------
|
|
183
183
|
|
|
184
184
|
async function _openRun(query, state, source, inputByteCount) {
|
|
185
|
+
// Reserve the single in-flight slot SYNCHRONOUSLY — before the first await —
|
|
186
|
+
// so two concurrent runs (an operator double-submitting) can't both open. A
|
|
187
|
+
// second run is refused rather than silently overwriting the first's inflight
|
|
188
|
+
// handle, which would leave `cancelInflight` targeting the wrong run. The
|
|
189
|
+
// check + set have no await between them, so the reservation is race-free in
|
|
190
|
+
// the single-threaded event loop.
|
|
191
|
+
if (state.inflight) {
|
|
192
|
+
var busy = new Error("customer-import: an import is already in progress");
|
|
193
|
+
busy.code = "IMPORT_IN_PROGRESS";
|
|
194
|
+
throw busy;
|
|
195
|
+
}
|
|
185
196
|
var runId = b.uuid.v7();
|
|
186
197
|
var startedAt = Date.now();
|
|
187
|
-
await query(
|
|
188
|
-
"INSERT INTO customer_imports " +
|
|
189
|
-
"(id, started_at, completed_at, status, source, input_byte_count, " +
|
|
190
|
-
" rows_processed, rows_created, rows_updated, rows_skipped, rows_errored, errors_json) " +
|
|
191
|
-
"VALUES (?1, ?2, NULL, 'running', ?3, ?4, 0, 0, 0, 0, 0, '[]')",
|
|
192
|
-
[runId, startedAt, source, inputByteCount],
|
|
193
|
-
);
|
|
194
198
|
state.inflight = { runId: runId, cancelled: false };
|
|
199
|
+
try {
|
|
200
|
+
await query(
|
|
201
|
+
"INSERT INTO customer_imports " +
|
|
202
|
+
"(id, started_at, completed_at, status, source, input_byte_count, " +
|
|
203
|
+
" rows_processed, rows_created, rows_updated, rows_skipped, rows_errored, errors_json) " +
|
|
204
|
+
"VALUES (?1, ?2, NULL, 'running', ?3, ?4, 0, 0, 0, 0, 0, '[]')",
|
|
205
|
+
[runId, startedAt, source, inputByteCount],
|
|
206
|
+
);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
state.inflight = null; // release the reservation if the open failed
|
|
209
|
+
throw e;
|
|
210
|
+
}
|
|
195
211
|
return { runId: runId, startedAt: startedAt };
|
|
196
212
|
}
|
|
197
213
|
|
|
@@ -205,7 +221,14 @@ async function _closeRun(query, state, runId, status, counters, errors) {
|
|
|
205
221
|
"SELECT status FROM customer_imports WHERE id = ?1",
|
|
206
222
|
[runId],
|
|
207
223
|
)).rows[0];
|
|
208
|
-
|
|
224
|
+
// Treat the run as cancelled if EITHER the row already says so OR the
|
|
225
|
+
// in-memory flag for THIS run is set. The flag path covers a cancel that
|
|
226
|
+
// landed in the reservation→INSERT window: `cancelInflight` flipped the flag
|
|
227
|
+
// and ran an UPDATE that matched no row yet (the run row wasn't inserted), so
|
|
228
|
+
// reading the DB status alone would finalize a cancelled run as complete.
|
|
229
|
+
var cancelled = (current && current.status === "cancelled") ||
|
|
230
|
+
!!(state.inflight && state.inflight.runId === runId && state.inflight.cancelled);
|
|
231
|
+
var finalStatus = cancelled ? "cancelled" : status;
|
|
209
232
|
var errorsJson = b.safeJson.stringify(errors, { canonical: true });
|
|
210
233
|
await query(
|
|
211
234
|
"UPDATE customer_imports SET " +
|
package/lib/product-import.js
CHANGED
|
@@ -327,20 +327,35 @@ async function _drive(query, catalog, state, input, driveOpts) {
|
|
|
327
327
|
// Per-row normalization errors land in the error list at the
|
|
328
328
|
// matching row_index; the row is skipped for catalog writes but
|
|
329
329
|
// still counted in `rows_processed`.
|
|
330
|
+
// Reserve the single in-flight slot SYNCHRONOUSLY — before the INSERT await —
|
|
331
|
+
// so two concurrent runs (an operator double-submitting) can't both open. A
|
|
332
|
+
// second run is refused rather than overwriting the first's inflight id,
|
|
333
|
+
// which would leave `cancelInflight` targeting the wrong import. The check +
|
|
334
|
+
// set have no await between them, so the reservation is race-free.
|
|
335
|
+
if (state.inflightId) {
|
|
336
|
+
var busy = new Error("product-import: an import is already in progress");
|
|
337
|
+
busy.code = "IMPORT_IN_PROGRESS";
|
|
338
|
+
throw busy;
|
|
339
|
+
}
|
|
330
340
|
var importId = b.uuid.v7();
|
|
331
341
|
var startedAt = Date.now();
|
|
332
342
|
var inputByteCount = driveOpts.inputByteCount == null ? 0 : driveOpts.inputByteCount;
|
|
343
|
+
state.inflightId = importId;
|
|
344
|
+
state.cancelFlag = false;
|
|
333
345
|
|
|
334
346
|
// Run row goes in at `running`. The driver updates the counters +
|
|
335
347
|
// status at the end. The error rows go in as they're encountered
|
|
336
348
|
// — operators tailing `errorsForImport` see progress mid-run.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
349
|
+
try {
|
|
350
|
+
await query(
|
|
351
|
+
"INSERT INTO product_imports (id, format, on_conflict, started_at, status, input_byte_count) " +
|
|
352
|
+
"VALUES (?1, ?2, ?3, ?4, 'running', ?5)",
|
|
353
|
+
[importId, format, onConflict, startedAt, inputByteCount],
|
|
354
|
+
);
|
|
355
|
+
} catch (e) {
|
|
356
|
+
state.inflightId = null; // release the reservation if the open failed
|
|
357
|
+
throw e;
|
|
358
|
+
}
|
|
344
359
|
|
|
345
360
|
var counters = {
|
|
346
361
|
rows_processed: 0,
|
package/package.json
CHANGED