@blamejs/blamejs-shop 0.3.35 → 0.3.36
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/admin.js +375 -1
- package/lib/asset-manifest.json +1 -1
- package/lib/storefront.js +450 -0
- package/lib/vendor/MANIFEST.json +3 -3
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +14 -2
- package/lib/vendor/blamejs/lib/crypto-field.js +69 -0
- package/lib/vendor/blamejs/lib/mail-store-fts.js +40 -18
- package/lib/vendor/blamejs/lib/mail-store.js +188 -2
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.10.json +54 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +17 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +44 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-store-fts.test.js +208 -0
- package/package.json +1 -1
|
@@ -240,6 +240,212 @@ async function testQueryKeyMapping() {
|
|
|
240
240
|
b.mailStore.fts.columnAndFieldFor("unknown") === null);
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
// Compute the legacy (v1) salted-sha3-truncated token hash exactly the
|
|
244
|
+
// way the pre-keyed mail-store-fts.hashToken did: sha3Hash(saltHex + ns
|
|
245
|
+
// + token).slice(0, 16). Used to forge an old-format FTS index so the
|
|
246
|
+
// reindex-on-upgrade path has a stale index to rebuild.
|
|
247
|
+
function _legacyHashToken(table, field, token) {
|
|
248
|
+
if (typeof token !== "string" || token.length === 0) return "";
|
|
249
|
+
var ns = "bj-" + table + "-" + field + ":fts:";
|
|
250
|
+
var saltHex = b.vault.getDerivedHashSalt().toString("hex");
|
|
251
|
+
return b.crypto.sha3Hash(saltHex + ns + token).slice(0, 16);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function _legacyHashTokens(table, field, tokens) {
|
|
255
|
+
var seen = Object.create(null);
|
|
256
|
+
var out = [];
|
|
257
|
+
for (var i = 0; i < tokens.length; i += 1) {
|
|
258
|
+
var h = _legacyHashToken(table, field, tokens[i]);
|
|
259
|
+
if (!h || seen[h]) continue;
|
|
260
|
+
seen[h] = true;
|
|
261
|
+
out.push(h);
|
|
262
|
+
}
|
|
263
|
+
return out.join(" ");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Rewrite the FTS index into the legacy (v1) format AND set the format
|
|
267
|
+
// marker stale, simulating an on-disk index written by a pre-keyed
|
|
268
|
+
// build. Reads the sealed messages table, unseals each row, retokenizes
|
|
269
|
+
// the plaintext, and reinserts under the legacy salted-sha3 hash.
|
|
270
|
+
// `markerValue` selects the stale marker: "1" (old format) or null
|
|
271
|
+
// (no marker row at all - pre-format-version store).
|
|
272
|
+
function _forgeLegacyIndex(db, prefix, table, markerValue) {
|
|
273
|
+
var ftsTable = '"' + prefix + '_messages_fts"';
|
|
274
|
+
var msgsTable = '"' + prefix + '_messages"';
|
|
275
|
+
var metaTable = '"' + prefix + '_meta"';
|
|
276
|
+
var rows = db.prepare("SELECT * FROM " + msgsTable).all();
|
|
277
|
+
db.prepare("DELETE FROM " + ftsTable).run();
|
|
278
|
+
var insert = db.prepare("INSERT INTO " + ftsTable +
|
|
279
|
+
" (objectid, subject_toks, addr_toks, body_toks) VALUES (?, ?, ?, ?)");
|
|
280
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
281
|
+
var clear = b.cryptoField.unsealRow(table, rows[i]);
|
|
282
|
+
var subjTokens = b.mailStore.fts.tokenize(clear.subject || "");
|
|
283
|
+
var addrTokens = b.mailStore.fts.tokenize(clear.from_addr || "")
|
|
284
|
+
.concat(b.mailStore.fts.tokenize(clear.to_addrs || ""));
|
|
285
|
+
var bodyTokens = b.mailStore.fts.tokenize(clear.body_text || "");
|
|
286
|
+
insert.run(
|
|
287
|
+
clear.objectid,
|
|
288
|
+
_legacyHashTokens(table, "subject", subjTokens),
|
|
289
|
+
_legacyHashTokens(table, "addr", addrTokens),
|
|
290
|
+
_legacyHashTokens(table, "body", bodyTokens)
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
if (markerValue === null) {
|
|
294
|
+
db.prepare("DELETE FROM " + metaTable + " WHERE key = ?").run("fts_format");
|
|
295
|
+
} else {
|
|
296
|
+
db.prepare("INSERT INTO " + metaTable + " (key, value) VALUES (?, ?) " +
|
|
297
|
+
"ON CONFLICT(key) DO UPDATE SET value = excluded.value").run("fts_format", markerValue);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function _readMarker(db, prefix) {
|
|
302
|
+
var row = db.prepare('SELECT value FROM "' + prefix + '_meta" WHERE key = ?').get("fts_format");
|
|
303
|
+
return row ? row.value : null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function testFtsReindexOnUpgrade() {
|
|
307
|
+
// Forge an old-format (v1 salted-sha3) index, then construct a fresh
|
|
308
|
+
// store: create() must detect the stale marker, rebuild the index
|
|
309
|
+
// from the sealed messages table under the new keyed hash, find the
|
|
310
|
+
// seeded docs again, and advance the marker to the current format.
|
|
311
|
+
var fx = await _setupStore("reindex");
|
|
312
|
+
try {
|
|
313
|
+
var store = b.mailStore.create({ backend: fx.db });
|
|
314
|
+
store.appendMessage("INBOX", _msg("alice@example.com", "bob@example.com",
|
|
315
|
+
"Kubernetes deploy plan",
|
|
316
|
+
"We should deploy on kubernetes next monday."));
|
|
317
|
+
store.appendMessage("INBOX", _msg("carol@example.com", "bob@example.com", "Lunch?",
|
|
318
|
+
"Wanna grab lunch this week?"));
|
|
319
|
+
|
|
320
|
+
check("fresh store stamps current marker",
|
|
321
|
+
_readMarker(fx.db, fx.prefix) === String(b.mailStore.fts.FTS_FORMAT_VERSION));
|
|
322
|
+
|
|
323
|
+
// Rewrite the index into the legacy format + stale marker.
|
|
324
|
+
_forgeLegacyIndex(fx.db, fx.prefix, fx.prefix + "_messages", "1");
|
|
325
|
+
check("forged marker is stale", _readMarker(fx.db, fx.prefix) === "1");
|
|
326
|
+
|
|
327
|
+
// A new store handle against the same db triggers the reindex.
|
|
328
|
+
var store2 = b.mailStore.create({ backend: fx.db });
|
|
329
|
+
|
|
330
|
+
check("marker advanced to current after reindex",
|
|
331
|
+
_readMarker(fx.db, fx.prefix) === String(b.mailStore.fts.FTS_FORMAT_VERSION));
|
|
332
|
+
|
|
333
|
+
// Search now finds the seeded docs under the rebuilt keyed index.
|
|
334
|
+
var r1 = store2.search("INBOX", { text: "kubernetes" });
|
|
335
|
+
check("reindexed: text=kubernetes hits 1 row", r1.rows.length === 1);
|
|
336
|
+
check("reindexed: not flagged ftsUnavailable", r1.ftsUnavailable !== true);
|
|
337
|
+
var r2 = store2.search("INBOX", { text: "lunch" });
|
|
338
|
+
check("reindexed: text=lunch hits 1 row", r2.rows.length === 1);
|
|
339
|
+
var r3 = store2.search("INBOX", { from: "alice@example.com" });
|
|
340
|
+
check("reindexed: from=alice hits 1 row", r3.rows.length === 1);
|
|
341
|
+
|
|
342
|
+
// Idempotent - a third construction sees the current marker and does
|
|
343
|
+
// not rebuild (search still works).
|
|
344
|
+
var store3 = b.mailStore.create({ backend: fx.db });
|
|
345
|
+
var r4 = store3.search("INBOX", { text: "kubernetes" });
|
|
346
|
+
check("idempotent reindex: still 1 row", r4.rows.length === 1);
|
|
347
|
+
} finally { _teardown(fx); }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function testFtsReindexRollsBackOnInterruption() {
|
|
351
|
+
// Inject a failure into the reindex INSERT LOOP: the Nth FTS insert
|
|
352
|
+
// throws. The whole rebuild must roll back, leaving the OLD index
|
|
353
|
+
// intact + queryable and the marker NOT advanced - a retriable state,
|
|
354
|
+
// never a silently half-built index.
|
|
355
|
+
var fx = await _setupStore("interrupt");
|
|
356
|
+
try {
|
|
357
|
+
var store = b.mailStore.create({ backend: fx.db });
|
|
358
|
+
store.appendMessage("INBOX", _msg("alice@example.com", "bob@example.com",
|
|
359
|
+
"Kubernetes deploy plan",
|
|
360
|
+
"We should deploy on kubernetes next monday."));
|
|
361
|
+
store.appendMessage("INBOX", _msg("carol@example.com", "bob@example.com",
|
|
362
|
+
"Picnic plan", "lets plan a picnic on saturday."));
|
|
363
|
+
|
|
364
|
+
var table = fx.prefix + "_messages";
|
|
365
|
+
_forgeLegacyIndex(fx.db, fx.prefix, table, "1");
|
|
366
|
+
|
|
367
|
+
// Snapshot the forged (old-format) index so we can prove it survives
|
|
368
|
+
// the rolled-back rebuild byte-for-byte.
|
|
369
|
+
var ftsTable = '"' + fx.prefix + '_messages_fts"';
|
|
370
|
+
var beforeRows = fx.db.prepare("SELECT objectid, subject_toks, addr_toks, body_toks FROM " +
|
|
371
|
+
ftsTable + " ORDER BY objectid").all();
|
|
372
|
+
check("forged index has 2 rows", beforeRows.length === 2);
|
|
373
|
+
|
|
374
|
+
// A pre-rebuild MATCH against the OLD keyed scheme must hit. Compute
|
|
375
|
+
// the legacy hash for "kubernetes" in the body namespace.
|
|
376
|
+
var legacyKube = _legacyHashToken(table, "body", "kubernetes");
|
|
377
|
+
var preHit = fx.db.prepare("SELECT objectid FROM " + ftsTable + " WHERE " +
|
|
378
|
+
ftsTable + " MATCH ?").all("body_toks:(" + legacyKube + ")");
|
|
379
|
+
check("old index queryable pre-interruption", preHit.length === 1);
|
|
380
|
+
|
|
381
|
+
// Wrap the backend so the SECOND FTS insert in the reindex loop
|
|
382
|
+
// throws. Every other call delegates to the real handle - this is a
|
|
383
|
+
// thin fault-injection shim around the live db, not a mock of the
|
|
384
|
+
// store logic.
|
|
385
|
+
var ftsInsertCalls = 0;
|
|
386
|
+
var wrapped = {
|
|
387
|
+
prepare: function (sql) {
|
|
388
|
+
var stmt = fx.db.prepare(sql);
|
|
389
|
+
if (/INSERT INTO .*_messages_fts/.test(sql)) {
|
|
390
|
+
return {
|
|
391
|
+
run: function () {
|
|
392
|
+
ftsInsertCalls += 1;
|
|
393
|
+
if (ftsInsertCalls === 2) {
|
|
394
|
+
throw new Error("injected backend failure on the 2nd FTS insert");
|
|
395
|
+
}
|
|
396
|
+
return stmt.run.apply(stmt, arguments);
|
|
397
|
+
},
|
|
398
|
+
get: function () { return stmt.get.apply(stmt, arguments); },
|
|
399
|
+
all: function () { return stmt.all.apply(stmt, arguments); },
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
return stmt;
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
var threw = false;
|
|
407
|
+
try {
|
|
408
|
+
b.mailStore.create({ backend: wrapped });
|
|
409
|
+
} catch (e) {
|
|
410
|
+
threw = true;
|
|
411
|
+
check("reindex failure surfaces as MailStoreError",
|
|
412
|
+
e && e.name === "MailStoreError");
|
|
413
|
+
}
|
|
414
|
+
check("interrupted reindex throws", threw);
|
|
415
|
+
|
|
416
|
+
// Marker did NOT advance to the current format - it's the sentinel
|
|
417
|
+
// (written before the DELETE), so a later create() retries.
|
|
418
|
+
check("marker did not advance to current",
|
|
419
|
+
_readMarker(fx.db, fx.prefix) !== String(b.mailStore.fts.FTS_FORMAT_VERSION));
|
|
420
|
+
|
|
421
|
+
// The OLD index survives the rollback intact, byte-for-byte.
|
|
422
|
+
var afterRows = fx.db.prepare("SELECT objectid, subject_toks, addr_toks, body_toks FROM " +
|
|
423
|
+
ftsTable + " ORDER BY objectid").all();
|
|
424
|
+
check("old index row count intact after rollback", afterRows.length === beforeRows.length);
|
|
425
|
+
var identical = afterRows.length === beforeRows.length;
|
|
426
|
+
for (var i = 0; i < afterRows.length && identical; i += 1) {
|
|
427
|
+
identical = afterRows[i].objectid === beforeRows[i].objectid &&
|
|
428
|
+
afterRows[i].subject_toks === beforeRows[i].subject_toks &&
|
|
429
|
+
afterRows[i].addr_toks === beforeRows[i].addr_toks &&
|
|
430
|
+
afterRows[i].body_toks === beforeRows[i].body_toks;
|
|
431
|
+
}
|
|
432
|
+
check("old index rows unchanged after rollback", identical);
|
|
433
|
+
|
|
434
|
+
// And the OLD index is still queryable under the legacy scheme.
|
|
435
|
+
var postHit = fx.db.prepare("SELECT objectid FROM " + ftsTable + " WHERE " +
|
|
436
|
+
ftsTable + " MATCH ?").all("body_toks:(" + legacyKube + ")");
|
|
437
|
+
check("old index still queryable after rollback", postHit.length === 1);
|
|
438
|
+
|
|
439
|
+
// Retriable: a clean create() against the real backend now completes
|
|
440
|
+
// the rebuild and advances the marker.
|
|
441
|
+
var store2 = b.mailStore.create({ backend: fx.db });
|
|
442
|
+
check("retry advances marker to current",
|
|
443
|
+
_readMarker(fx.db, fx.prefix) === String(b.mailStore.fts.FTS_FORMAT_VERSION));
|
|
444
|
+
var r = store2.search("INBOX", { text: "kubernetes" });
|
|
445
|
+
check("retry: search finds the doc", r.rows.length === 1);
|
|
446
|
+
} finally { _teardown(fx); }
|
|
447
|
+
}
|
|
448
|
+
|
|
243
449
|
async function run() {
|
|
244
450
|
// One-time vault init for the sync tokenizer + hash tests — the
|
|
245
451
|
// hash routine reads vault.getDerivedHashSalt() so the salt MUST
|
|
@@ -263,6 +469,8 @@ async function run() {
|
|
|
263
469
|
await testFtsRowIsSealed();
|
|
264
470
|
await testSearchRespectsLimitCap();
|
|
265
471
|
await testQueryKeyMapping();
|
|
472
|
+
await testFtsReindexOnUpgrade();
|
|
473
|
+
await testFtsReindexRollsBackOnInterruption();
|
|
266
474
|
|
|
267
475
|
try { nodeFs.rmSync(bootDir, { recursive: true, force: true }); } catch (_e) {}
|
|
268
476
|
|
package/package.json
CHANGED