sphica 0.5.6 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3193,24 +3193,32 @@ var MYSQL_PASSWORD = /(\s-p)(?:'[^'\n]*'|"[^"\n]*"|(?=[^\s-])\S+)/;
3193
3193
  var URL_CREDENTIALS = /\b((?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|rediss?|amqps?|https?):\/\/[^:\s/@]*:)[^\s/]*@([^@\s/?#]+)/g;
3194
3194
  var KEY_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/g;
3195
3195
  var KEY_END = /-----END [A-Z ]*PRIVATE KEY-----/g;
3196
- function maskPrivateKeys(text) {
3196
+ function keyBlocks(text) {
3197
3197
  const ends = [...text.matchAll(KEY_END)].map((m) => [m.index, m.index + m[0].length]);
3198
- if (ends.length === 0)
3199
- return text;
3200
- let out = "";
3198
+ const out = [];
3201
3199
  let last = 0;
3202
3200
  let e = 0;
3203
- for (const m of text.matchAll(KEY_BEGIN)) {
3204
- const after = m.index + m[0].length;
3205
- if (m.index < last)
3206
- continue;
3207
- while (e < ends.length && (ends[e]?.[0] ?? 0) < after)
3208
- e++;
3209
- const end = ends[e];
3210
- if (!end)
3211
- break;
3212
- out += `${text.slice(last, m.index)}[redacted: private key]`;
3213
- last = end[1];
3201
+ if (ends.length > 0)
3202
+ for (const m of text.matchAll(KEY_BEGIN)) {
3203
+ const after = m.index + m[0].length;
3204
+ if (m.index < last)
3205
+ continue;
3206
+ while (e < ends.length && (ends[e]?.[0] ?? 0) < after)
3207
+ e++;
3208
+ const end = ends[e];
3209
+ if (!end)
3210
+ break;
3211
+ out.push([m.index, end[1]]);
3212
+ last = end[1];
3213
+ }
3214
+ return out;
3215
+ }
3216
+ function maskPrivateKeys(text) {
3217
+ let out = "";
3218
+ let last = 0;
3219
+ for (const [a2, b] of keyBlocks(text)) {
3220
+ out += `${text.slice(last, a2)}[redacted: private key]`;
3221
+ last = b;
3214
3222
  }
3215
3223
  return out + text.slice(last);
3216
3224
  }
@@ -14179,7 +14187,7 @@ import os from "node:os";
14179
14187
  import path2 from "node:path";
14180
14188
  import { constants as C, DatabaseSync } from "node:sqlite";
14181
14189
  var SCHEMA_GENERATION = 2;
14182
- var SCHEMA_REVISION = 1;
14190
+ var SCHEMA_REVISION = 2;
14183
14191
  var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
14184
14192
  var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
14185
14193
  function requireRuntime() {
@@ -14203,7 +14211,7 @@ function prepare(raw, check) {
14203
14211
  const got = raw.prepare("pragma user_version").get()?.user_version;
14204
14212
  if (got === SCHEMA_REVISION)
14205
14213
  return;
14206
- throw new Error(`The database schema is revision ${got}, but this Sphica expects revision ${SCHEMA_REVISION}. ` + ((got ?? 0) < SCHEMA_REVISION ? "Move it aside (it is left unchanged), then run `sphica init`." : "Update sphica."));
14214
+ throw new Error(`The database schema is revision ${got}, but this Sphica expects revision ${SCHEMA_REVISION}. ` + ((got ?? 0) < SCHEMA_REVISION ? "Update the sphica CLI (`npm i -g sphica`), then run `sphica init` to migrate it (records are kept)." : "Update sphica."));
14207
14215
  }
14208
14216
  function generationOf(raw) {
14209
14217
  const has = raw.prepare("select 1 from sqlite_schema where type = 'table' and name = 'sphica_generation'").get();
@@ -14375,22 +14383,54 @@ function captureAuthorizer(own, action, p1, p2, triggerOrView) {
14375
14383
  return C2.SQLITE_DENY;
14376
14384
  }
14377
14385
  var writes = (action) => action === C2.SQLITE_INSERT || action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE;
14386
+ var FORGET_ONLY = new Set(["forget_batch", "source_forgotten"]);
14387
+ var KEPT_BY_INGEST = new Set(["source", "unit_evidence", "unit_adoption"]);
14378
14388
  function ingestAuthorizer(action, p1, p2) {
14379
14389
  if (DDL().has(action))
14380
14390
  return C2.SQLITE_DENY;
14381
- if (writes(action) && p1 === "owner_identity")
14391
+ if (writes(action) && (p1 === "owner_identity" || FORGET_ONLY.has(p1 ?? "")))
14392
+ return C2.SQLITE_DENY;
14393
+ if (action === C2.SQLITE_DELETE && KEPT_BY_INGEST.has(p1 ?? ""))
14382
14394
  return C2.SQLITE_DENY;
14383
14395
  if (action === C2.SQLITE_PRAGMA)
14384
14396
  return readsDataVersion(p1, p2) ? C2.SQLITE_OK : C2.SQLITE_DENY;
14385
14397
  return C2.SQLITE_OK;
14386
14398
  }
14399
+ var FORGET_WRITES = {
14400
+ [C2.SQLITE_INSERT]: new Set(["forget_batch", "source_forgotten", "unit_state", "source_fts"]),
14401
+ [C2.SQLITE_DELETE]: new Set([
14402
+ "source",
14403
+ "external_reference",
14404
+ "unit_evidence",
14405
+ "unit_adoption",
14406
+ "source_processing",
14407
+ "source_fts"
14408
+ ])
14409
+ };
14410
+ var FORGET_UPDATES = {
14411
+ unit: new Set(["lifecycle", "revision"]),
14412
+ unit_state: new Set(["source_id"])
14413
+ };
14414
+ function forgetAuthorizer(action, p1, p2) {
14415
+ if (SHADOW.test(p1 ?? "") && writes(action))
14416
+ return C2.SQLITE_OK;
14417
+ if (action === C2.SQLITE_UPDATE)
14418
+ return FORGET_UPDATES[p1 ?? ""]?.has(p2 ?? "") ? C2.SQLITE_OK : C2.SQLITE_DENY;
14419
+ if (action === C2.SQLITE_INSERT || action === C2.SQLITE_DELETE)
14420
+ return FORGET_WRITES[action]?.has(p1 ?? "") ? C2.SQLITE_OK : C2.SQLITE_DENY;
14421
+ if (action === C2.SQLITE_PRAGMA)
14422
+ return readsDataVersion(p1, p2) || p1 === "secure_delete" && ["on", "1", "true"].includes((p2 ?? "").toLowerCase()) || p1 === "wal_checkpoint" ? C2.SQLITE_OK : C2.SQLITE_DENY;
14423
+ if (action === C2.SQLITE_READ || action === C2.SQLITE_SELECT || action === C2.SQLITE_FUNCTION || action === C2.SQLITE_TRANSACTION || action === C2.SQLITE_SAVEPOINT)
14424
+ return C2.SQLITE_OK;
14425
+ return C2.SQLITE_DENY;
14426
+ }
14387
14427
  function connectWriter(role, file = dbFile(), create = false) {
14388
14428
  requireRuntime();
14389
14429
  if (!create)
14390
14430
  requireFile(file);
14391
14431
  const raw = new DatabaseSync2(file);
14392
14432
  try {
14393
- prepare(raw, create ? "none" : role === "ingest" ? "revision" : "generation");
14433
+ prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation");
14394
14434
  raw.function("sphica_terms", { deterministic: true }, (text) => terms(String(text ?? "")).join(" "));
14395
14435
  } catch (e) {
14396
14436
  raw.close();
@@ -14398,6 +14438,8 @@ function connectWriter(role, file = dbFile(), create = false) {
14398
14438
  }
14399
14439
  if (role === "ingest")
14400
14440
  raw.setAuthorizer(ingestAuthorizer);
14441
+ else if (role === "forget")
14442
+ raw.setAuthorizer(forgetAuthorizer);
14401
14443
  else if (role === "capture") {
14402
14444
  let own = false;
14403
14445
  raw.setAuthorizer((action, p1, p2, _db, triggerOrView) => captureAuthorizer(own, action, p1, p2, triggerOrView));
@@ -14423,6 +14465,7 @@ function openWriter(role, file = dbFile()) {
14423
14465
  // server/src/admin.ts
14424
14466
  var say = (text) => console.log(indent(text));
14425
14467
  var SCHEMA = () => path3.join(dbDir(), "schema.sql");
14468
+ var MIGRATION = (revision) => path3.join(dbDir(), "migrations", `${String(revision).padStart(4, "0")}.sql`);
14426
14469
  var versionOf = (raw) => raw.prepare("pragma user_version").get().user_version;
14427
14470
  function immediate(raw, fn) {
14428
14471
  raw.exec("begin immediate");
@@ -14443,6 +14486,32 @@ function withOwner(file, fn, create = false) {
14443
14486
  raw.close();
14444
14487
  }
14445
14488
  }
14489
+ function migrate(file = dbFile()) {
14490
+ return withOwner(file, (raw) => {
14491
+ const from = versionOf(raw);
14492
+ for (let r2 = from + 1;r2 <= SCHEMA_REVISION; r2++) {
14493
+ const script = MIGRATION(r2);
14494
+ if (!fs3.existsSync(script))
14495
+ throw new Error(`No migration from revision ${r2 - 1}. Move the database aside, then run \`sphica init\`.`);
14496
+ raw.exec("pragma foreign_keys = off");
14497
+ try {
14498
+ immediate(raw, () => {
14499
+ if (versionOf(raw) !== r2 - 1)
14500
+ return;
14501
+ raw.exec(fs3.readFileSync(script, "utf8"));
14502
+ const broken = raw.prepare("pragma foreign_key_check").all().length;
14503
+ if (broken)
14504
+ throw new Error(`Migration to revision ${r2} left ${plural(broken, "broken reference")}; nothing was changed`);
14505
+ if (versionOf(raw) !== r2)
14506
+ throw new Error(`${path3.basename(script)} did not set revision ${r2}`);
14507
+ });
14508
+ } finally {
14509
+ raw.exec("pragma foreign_keys = on");
14510
+ }
14511
+ }
14512
+ return from;
14513
+ });
14514
+ }
14446
14515
  function dbInit(file = dbFile()) {
14447
14516
  if (fs3.existsSync(file)) {
14448
14517
  const look = new DatabaseSync3(file, { readOnly: true });
@@ -14457,8 +14526,12 @@ function dbInit(file = dbFile()) {
14457
14526
  const got = withOwner(file, versionOf);
14458
14527
  if (got === SCHEMA_REVISION)
14459
14528
  say(`Already exists: ${file} (revision ${got})`);
14460
- else
14461
- say(`Already exists: ${file} (revision ${got}; this Sphica expects ${SCHEMA_REVISION}. Move it aside, then run \`sphica init\`.)`);
14529
+ else if (got > SCHEMA_REVISION)
14530
+ say(`Already exists: ${file} (revision ${got}, made by a newer Sphica; this one expects ${SCHEMA_REVISION}. Update sphica.)`);
14531
+ else {
14532
+ migrate(file);
14533
+ say(`Migrated: ${file} (revision ${got} → ${SCHEMA_REVISION})`);
14534
+ }
14462
14535
  return;
14463
14536
  }
14464
14537
  fs3.mkdirSync(path3.dirname(file), { recursive: true, mode: 448 });
@@ -15728,7 +15801,7 @@ async function doctor(cwd) {
15728
15801
  const x = inspect(file);
15729
15802
  usable2 = x.revision === SCHEMA_REVISION;
15730
15803
  say2("ok", "DB", `${file} (${(x.bytes / 1024 / 1024).toFixed(1)} MB)`);
15731
- say2(usable2 ? "ok" : "fail", "Schema version", usable2 ? `revision ${x.revision}` : `revision ${x.revision}, this Sphica expects ${SCHEMA_REVISION} (${x.revision < SCHEMA_REVISION ? "made by an older Sphica: move it aside, then run sphica init" : "update sphica"})`);
15804
+ say2(usable2 ? "ok" : "fail", "Schema version", usable2 ? `revision ${x.revision}` : `revision ${x.revision}, this Sphica expects ${SCHEMA_REVISION} (${x.revision < SCHEMA_REVISION ? "made by an older Sphica: run sphica init to migrate it" : "update sphica"})`);
15732
15805
  const broken = Object.entries(x.fts).filter(([, v]) => v !== null);
15733
15806
  say2(broken.length ? "fail" : "ok", "Full-text index", broken.length ? `broken: ${broken.map(([k, v]) => `${k} (${plain(v ?? "")})`).join(" / ")}. Rebuild it with sphica doctor --reindex` : "healthy");
15734
15807
  } catch (e) {