latticesql 3.4.3 → 3.4.4

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/index.js CHANGED
@@ -231,10 +231,12 @@ function readManifest(outputDir) {
231
231
  function writeManifest(outputDir, manifest) {
232
232
  atomicWrite(manifestPath(outputDir), JSON.stringify(manifest, null, 2));
233
233
  }
234
+ var TEMPLATE_VERSION;
234
235
  var init_manifest = __esm({
235
236
  "src/lifecycle/manifest.ts"() {
236
237
  "use strict";
237
238
  init_writer();
239
+ TEMPLATE_VERSION = 1;
238
240
  }
239
241
  });
240
242
 
@@ -268,6 +270,126 @@ var init_adapter = __esm({
268
270
  }
269
271
  });
270
272
 
273
+ // src/lifecycle/render-cursor.ts
274
+ function markToString(v2) {
275
+ if (v2 == null) return null;
276
+ if (v2 instanceof Date) return v2.toISOString();
277
+ if (typeof v2 === "string") return v2;
278
+ if (typeof v2 === "number" || typeof v2 === "bigint" || typeof v2 === "boolean") return String(v2);
279
+ return null;
280
+ }
281
+ function padNumericMark(v2) {
282
+ const s2 = markToString(v2);
283
+ if (s2 == null) return null;
284
+ if (/^\d+$/.test(s2)) return s2.padStart(20, "0");
285
+ return s2;
286
+ }
287
+ async function changelogExists(adapter) {
288
+ if (adapter.dialect === "postgres") {
289
+ const row2 = await getAsyncOrSync(
290
+ adapter,
291
+ `SELECT to_regclass('__lattice_changelog') AS reg`
292
+ );
293
+ return !!row2 && row2.reg != null;
294
+ }
295
+ const row = await getAsyncOrSync(
296
+ adapter,
297
+ `SELECT name FROM sqlite_master WHERE type='table' AND name='__lattice_changelog'`
298
+ );
299
+ return !!row;
300
+ }
301
+ async function changelogMark(adapter) {
302
+ try {
303
+ if (!await changelogExists(adapter)) return null;
304
+ const col = adapter.dialect === "postgres" ? "seq" : "rowid";
305
+ const row = await getAsyncOrSync(
306
+ adapter,
307
+ `SELECT MAX(${col}) AS m FROM __lattice_changelog`
308
+ );
309
+ return padNumericMark(row?.m);
310
+ } catch {
311
+ return null;
312
+ }
313
+ }
314
+ async function sharingMarks(adapter) {
315
+ if (adapter.dialect !== "postgres") return { grants: null, owners: null };
316
+ try {
317
+ const reg = await getAsyncOrSync(
318
+ adapter,
319
+ `SELECT to_regclass('__lattice_changes') AS reg`
320
+ );
321
+ const hasFeed = !!reg && reg.reg != null;
322
+ if (hasFeed) {
323
+ const row = await getAsyncOrSync(
324
+ adapter,
325
+ `SELECT COUNT(*) AS n, MAX(seq) AS m FROM lattice_changes_since(0, 1000)`
326
+ );
327
+ const digest = digestOf(row?.n, row?.m);
328
+ return { grants: digest, owners: digest };
329
+ }
330
+ } catch {
331
+ }
332
+ let owners = null;
333
+ let grants = null;
334
+ try {
335
+ const o3 = await getAsyncOrSync(
336
+ adapter,
337
+ `SELECT COUNT(*) AS n, MAX(updated_at) AS m FROM __lattice_owners`
338
+ );
339
+ owners = digestOf(o3?.n, o3?.m);
340
+ } catch {
341
+ owners = null;
342
+ }
343
+ try {
344
+ const g6 = await getAsyncOrSync(
345
+ adapter,
346
+ `SELECT COUNT(*) AS n, MAX(granted_at) AS m FROM __lattice_row_grants`
347
+ );
348
+ grants = digestOf(g6?.n, g6?.m);
349
+ } catch {
350
+ grants = null;
351
+ }
352
+ return { grants, owners };
353
+ }
354
+ function digestOf(count, max) {
355
+ const n3 = padNumericMark(count);
356
+ if (n3 == null) return null;
357
+ const m4 = markToString(max) ?? "";
358
+ return `${n3}#${m4}`;
359
+ }
360
+ async function computeRenderCursor(adapter) {
361
+ try {
362
+ const [changelog, sharing] = await Promise.all([changelogMark(adapter), sharingMarks(adapter)]);
363
+ return { changelog, grants: sharing.grants, owners: sharing.owners };
364
+ } catch {
365
+ return { ...EMPTY_CURSOR };
366
+ }
367
+ }
368
+ function cursorIsFresh(recorded, live, templateVersion = TEMPLATE_VERSION) {
369
+ if (recorded == null) return false;
370
+ if (recorded.templateVersion !== templateVersion) return false;
371
+ const rc = recorded.cursor;
372
+ if (rc == null) return false;
373
+ if (!fieldFresh(rc.changelog, live.changelog, (r6, l4) => l4 <= r6)) return false;
374
+ if (!fieldFresh(rc.grants, live.grants, (r6, l4) => l4 === r6)) return false;
375
+ if (!fieldFresh(rc.owners, live.owners, (r6, l4) => l4 === r6)) return false;
376
+ return true;
377
+ }
378
+ function fieldFresh(recorded, live, ok) {
379
+ if (recorded == null && live == null) return true;
380
+ if (recorded == null || live == null) return false;
381
+ return ok(recorded, live);
382
+ }
383
+ var EMPTY_CURSOR;
384
+ var init_render_cursor = __esm({
385
+ "src/lifecycle/render-cursor.ts"() {
386
+ "use strict";
387
+ init_adapter();
388
+ init_manifest();
389
+ EMPTY_CURSOR = { changelog: null, grants: null, owners: null };
390
+ }
391
+ });
392
+
271
393
  // src/db/sqlite.ts
272
394
  import Database from "better-sqlite3";
273
395
  var SQLiteAdapter;
@@ -2310,7 +2432,18 @@ var init_concurrency = __esm({
2310
2432
  // src/render/engine.ts
2311
2433
  import { join as join4, basename, isAbsolute, resolve, sep } from "path";
2312
2434
  import { mkdirSync as mkdirSync2, existsSync as existsSync4, copyFileSync as copyFileSync2 } from "fs";
2313
- var YIELD_EVERY_ENTITIES, RENDER_TABLE_CONCURRENCY, NOOP_RENDER, RenderEngine;
2435
+ function entityContentChanged(fresh, prior) {
2436
+ const freshKeys = Object.keys(fresh);
2437
+ const priorKeys = Object.keys(prior);
2438
+ if (freshKeys.length !== priorKeys.length) return true;
2439
+ for (const k6 of freshKeys) {
2440
+ const p3 = prior[k6];
2441
+ if (p3 == null) return true;
2442
+ if (p3.hash === "" || p3.hash !== fresh[k6]?.hash) return true;
2443
+ }
2444
+ return false;
2445
+ }
2446
+ var DeferredTableProgress, YIELD_EVERY_ENTITIES, RENDER_TABLE_CONCURRENCY, NOOP_RENDER, RenderEngine;
2314
2447
  var init_engine = __esm({
2315
2448
  "src/render/engine.ts"() {
2316
2449
  "use strict";
@@ -2320,9 +2453,44 @@ var init_engine = __esm({
2320
2453
  init_entity_query();
2321
2454
  init_entity_templates();
2322
2455
  init_manifest();
2456
+ init_render_cursor();
2323
2457
  init_cleanup();
2324
2458
  init_progress();
2325
2459
  init_concurrency();
2460
+ DeferredTableProgress = class {
2461
+ constructor(throttle) {
2462
+ this.throttle = throttle;
2463
+ }
2464
+ changed = false;
2465
+ pendingStart = null;
2466
+ /** Buffer the `table-start` event; emitted only if/when the table changes. */
2467
+ start(event) {
2468
+ if (this.changed) {
2469
+ this.throttle.force(event);
2470
+ return;
2471
+ }
2472
+ this.pendingStart = event;
2473
+ }
2474
+ /** Mark that an entity's content changed — flush the held `table-start` once. */
2475
+ markChanged() {
2476
+ if (this.changed) return;
2477
+ this.changed = true;
2478
+ if (this.pendingStart) {
2479
+ this.throttle.force(this.pendingStart);
2480
+ this.pendingStart = null;
2481
+ }
2482
+ }
2483
+ /** Coalesced per-entity progress — dropped entirely until the table changed. */
2484
+ tick(event) {
2485
+ if (!this.changed) return;
2486
+ this.throttle.tick(event);
2487
+ }
2488
+ /** Lifecycle event (`table-done`) — emitted only if the table changed. */
2489
+ force(event) {
2490
+ if (!this.changed) return;
2491
+ this.throttle.force(event);
2492
+ }
2493
+ };
2326
2494
  YIELD_EVERY_ENTITIES = 200;
2327
2495
  RENDER_TABLE_CONCURRENCY = 4;
2328
2496
  NOOP_RENDER = () => "";
@@ -2439,20 +2607,23 @@ var init_engine = __esm({
2439
2607
  }
2440
2608
  const content = def.tokenBudget ? applyTokenBudget(rows, def.render, def.tokenBudget, def.prioritizeBy) : def.render(rows);
2441
2609
  const filePath = join4(outputDir, def.outputFile);
2442
- if (atomicWrite(filePath, content)) {
2610
+ const wrote = atomicWrite(filePath, content);
2611
+ if (wrote) {
2443
2612
  filesWritten.push(filePath);
2444
2613
  } else {
2445
2614
  counters.skipped++;
2446
2615
  }
2447
- throttle.force({
2448
- kind: "table-done",
2449
- table: name,
2450
- entitiesRendered: rows.length,
2451
- entitiesTotal: rows.length,
2452
- tableIndex: 0,
2453
- tableCount: 0,
2454
- pct: 100
2455
- });
2616
+ if (wrote) {
2617
+ throttle.force({
2618
+ kind: "table-done",
2619
+ table: name,
2620
+ entitiesRendered: rows.length,
2621
+ entitiesTotal: rows.length,
2622
+ tableIndex: 0,
2623
+ tableCount: 0,
2624
+ pct: 100
2625
+ });
2626
+ }
2456
2627
  }
2457
2628
  for (const [name, def] of this._schema.getMultis()) {
2458
2629
  if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
@@ -2466,32 +2637,38 @@ var init_engine = __esm({
2466
2637
  tables[t8] = await this._schema.queryTable(this._adapter, t8, this._readRel);
2467
2638
  }
2468
2639
  }
2640
+ let wroteAny = false;
2469
2641
  for (const key of keys) {
2470
2642
  const content = def.render(key, tables);
2471
2643
  const filePath = join4(outputDir, def.outputFile(key));
2472
2644
  if (atomicWrite(filePath, content)) {
2473
2645
  filesWritten.push(filePath);
2646
+ wroteAny = true;
2474
2647
  } else {
2475
2648
  counters.skipped++;
2476
2649
  }
2477
2650
  }
2478
- throttle.force({
2479
- kind: "table-done",
2480
- table: name,
2481
- entitiesRendered: keys.length,
2482
- entitiesTotal: keys.length,
2483
- tableIndex: 0,
2484
- tableCount: 0,
2485
- pct: 100
2486
- });
2651
+ if (wroteAny) {
2652
+ throttle.force({
2653
+ kind: "table-done",
2654
+ table: name,
2655
+ entitiesRendered: keys.length,
2656
+ entitiesTotal: keys.length,
2657
+ tableIndex: 0,
2658
+ tableCount: 0,
2659
+ pct: 100
2660
+ });
2661
+ }
2487
2662
  }
2663
+ const priorManifest = readManifest(outputDir);
2488
2664
  const entityContextManifest = await this._renderEntityContexts(
2489
2665
  outputDir,
2490
2666
  filesWritten,
2491
2667
  counters,
2492
2668
  throttle,
2493
2669
  signal,
2494
- opts.changedTables
2670
+ opts.changedTables,
2671
+ priorManifest
2495
2672
  );
2496
2673
  if (entityContextManifest === null) {
2497
2674
  return this._abortedResult(filesWritten, counters, start);
@@ -2502,10 +2679,13 @@ var init_engine = __esm({
2502
2679
  const prev = readManifest(outputDir);
2503
2680
  entityContexts = { ...prev?.entityContexts ?? {}, ...entityContextManifest };
2504
2681
  }
2682
+ const cursor = await computeRenderCursor(this._adapter);
2505
2683
  writeManifest(outputDir, {
2506
2684
  version: 2,
2507
2685
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
2508
- entityContexts
2686
+ entityContexts,
2687
+ templateVersion: TEMPLATE_VERSION,
2688
+ cursor
2509
2689
  });
2510
2690
  }
2511
2691
  const result = {
@@ -2571,7 +2751,7 @@ var init_engine = __esm({
2571
2751
  * partial tree). Progress is reported through `throttle`; abort is observed
2572
2752
  * via `signal`.
2573
2753
  */
2574
- async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal, changedTables) {
2754
+ async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal, changedTables, priorManifest) {
2575
2755
  const protectedTables = /* @__PURE__ */ new Set();
2576
2756
  for (const [t8, d6] of this._schema.getEntityContexts()) {
2577
2757
  if (d6.protected) protectedTables.add(t8);
@@ -2590,8 +2770,10 @@ var init_engine = __esm({
2590
2770
  const baseRows = await this._schema.queryTable(this._adapter, table, this._readRel);
2591
2771
  const allRows = this._foldRows ? await this._foldRows(table, baseRows) : baseRows;
2592
2772
  const directoryRoot = def.directoryRoot ?? table;
2773
+ const deferred = new DeferredTableProgress(throttle);
2774
+ const priorEntities = priorManifest?.entityContexts[table]?.entities ?? {};
2593
2775
  const entitiesTotal = allRows.length;
2594
- throttle.force({
2776
+ deferred.start({
2595
2777
  kind: "table-start",
2596
2778
  table,
2597
2779
  entitiesRendered: 0,
@@ -2600,6 +2782,7 @@ var init_engine = __esm({
2600
2782
  tableCount,
2601
2783
  pct: 0
2602
2784
  });
2785
+ if (Object.keys(priorEntities).length !== entitiesTotal) deferred.markChanged();
2603
2786
  const manifestEntry = {
2604
2787
  directoryRoot,
2605
2788
  ...def.index ? { indexFile: def.index.outputFile } : {},
@@ -2715,8 +2898,10 @@ var init_engine = __esm({
2715
2898
  }
2716
2899
  }
2717
2900
  manifestEntry.entities[slug] = entityFileHashes;
2901
+ const priorHashes = normalizeEntityFiles(priorEntities[slug] ?? {});
2902
+ if (entityContentChanged(entityFileHashes, priorHashes)) deferred.markChanged();
2718
2903
  const entitiesRendered = i6 + 1;
2719
- throttle.tick({
2904
+ deferred.tick({
2720
2905
  kind: "table-progress",
2721
2906
  table,
2722
2907
  entitiesRendered,
@@ -2726,7 +2911,7 @@ var init_engine = __esm({
2726
2911
  pct: entitiesTotal > 0 ? entitiesRendered / entitiesTotal * 100 : 100
2727
2912
  });
2728
2913
  }
2729
- throttle.force({
2914
+ deferred.force({
2730
2915
  kind: "table-done",
2731
2916
  table,
2732
2917
  entitiesRendered: entitiesTotal,
@@ -4166,6 +4351,22 @@ function deleteAssistantCredential(kind) {
4166
4351
  void _removed;
4167
4352
  saveAssistantCredentials(rest);
4168
4353
  }
4354
+ function isAssistantCredentialCleared(kind) {
4355
+ return loadAssistantCredentials()[CLEARED_SENTINEL_PREFIX + kind] === "1";
4356
+ }
4357
+ function setAssistantCredentialCleared(kind) {
4358
+ const creds = loadAssistantCredentials();
4359
+ creds[CLEARED_SENTINEL_PREFIX + kind] = "1";
4360
+ saveAssistantCredentials(creds);
4361
+ }
4362
+ function clearAssistantCredentialCleared(kind) {
4363
+ const creds = loadAssistantCredentials();
4364
+ const sentinel = CLEARED_SENTINEL_PREFIX + kind;
4365
+ if (!(sentinel in creds)) return;
4366
+ const { [sentinel]: _removed, ...rest } = creds;
4367
+ void _removed;
4368
+ saveAssistantCredentials(rest);
4369
+ }
4169
4370
  function ensureKeysDir() {
4170
4371
  const dir = join9(ensureConfigDir(), KEYS_SUBDIR);
4171
4372
  if (!existsSync9(dir)) {
@@ -4210,7 +4411,7 @@ function deleteToken(label) {
4210
4411
  const path2 = join9(ensureKeysDir(), label + TOKEN_EXT);
4211
4412
  if (existsSync9(path2)) unlinkSync3(path2);
4212
4413
  }
4213
- var MASTER_KEY_FILENAME, IDENTITY_FILENAME, EMPTY_IDENTITY, PREFERENCES_FILENAME, DEFAULT_PREFERENCES, DB_CREDENTIALS_FILENAME, CRED_LOCK_FILENAME, LOCK_STALE_MS, LOCK_TIMEOUT_MS, lockDepthInProcess, S3_CONFIG_FILENAME, ASSISTANT_CREDENTIALS_FILENAME, KEYS_SUBDIR, TOKEN_EXT;
4414
+ var MASTER_KEY_FILENAME, IDENTITY_FILENAME, EMPTY_IDENTITY, PREFERENCES_FILENAME, DEFAULT_PREFERENCES, DB_CREDENTIALS_FILENAME, CRED_LOCK_FILENAME, LOCK_STALE_MS, LOCK_TIMEOUT_MS, lockDepthInProcess, S3_CONFIG_FILENAME, ASSISTANT_CREDENTIALS_FILENAME, CLEARED_SENTINEL_PREFIX, KEYS_SUBDIR, TOKEN_EXT;
4214
4415
  var init_user_config = __esm({
4215
4416
  "src/framework/user-config.ts"() {
4216
4417
  "use strict";
@@ -4233,6 +4434,7 @@ var init_user_config = __esm({
4233
4434
  lockDepthInProcess = 0;
4234
4435
  S3_CONFIG_FILENAME = "s3-config.enc";
4235
4436
  ASSISTANT_CREDENTIALS_FILENAME = "assistant-credentials.enc";
4437
+ CLEARED_SENTINEL_PREFIX = "__cleared__:";
4236
4438
  KEYS_SUBDIR = "keys";
4237
4439
  TOKEN_EXT = ".token";
4238
4440
  }
@@ -5244,6 +5446,7 @@ var init_lattice = __esm({
5244
5446
  init_shred();
5245
5447
  init_encryption();
5246
5448
  init_manifest();
5449
+ init_render_cursor();
5247
5450
  init_adapter();
5248
5451
  init_sqlite();
5249
5452
  init_postgres();
@@ -5314,6 +5517,14 @@ var init_lattice = __esm({
5314
5517
  _changelogTables = /* @__PURE__ */ new Set();
5315
5518
  /** Current task context string for relevance filtering. */
5316
5519
  _taskContext = "";
5520
+ /**
5521
+ * True when this connection opened against an already-provisioned cloud as a
5522
+ * SCOPED MEMBER (no role-management privilege → no CREATE/ALTER on the schema).
5523
+ * Set during init() by the same probe that decides introspect-only. Drives
5524
+ * {@link addColumn} to route DDL through the owner-side `lattice_member_add_column`
5525
+ * SECURITY DEFINER helper instead of issuing a raw ALTER the member can't run.
5526
+ */
5527
+ _cloudMemberOpen = false;
5317
5528
  _auditHandlers = [];
5318
5529
  _renderHandlers = [];
5319
5530
  _writebackHandlers = [];
@@ -5560,7 +5771,7 @@ var init_lattice = __esm({
5560
5771
  /** Async tail of init(). See {@link init} for the sync-validation phase. */
5561
5772
  async _initAsync(options) {
5562
5773
  let introspectOnly = options.introspectOnly === true;
5563
- if (!introspectOnly && this.getDialect() === "postgres") {
5774
+ if (this.getDialect() === "postgres") {
5564
5775
  try {
5565
5776
  const [marker, role] = await Promise.all([
5566
5777
  getAsyncOrSync(this._adapter, `SELECT to_regclass('__lattice_owners') AS reg`),
@@ -5571,7 +5782,9 @@ var init_lattice = __esm({
5571
5782
  ]);
5572
5783
  const provisioned = !!marker && marker.reg != null;
5573
5784
  const canCreateRoles = !!role && role.rolcreaterole === true;
5574
- introspectOnly = provisioned && !canCreateRoles;
5785
+ const memberOpen = provisioned && !canCreateRoles;
5786
+ introspectOnly = introspectOnly || memberOpen;
5787
+ this._cloudMemberOpen = memberOpen;
5575
5788
  } catch {
5576
5789
  }
5577
5790
  }
@@ -5659,6 +5872,26 @@ var init_lattice = __esm({
5659
5872
  getDialect() {
5660
5873
  return this._adapter.dialect;
5661
5874
  }
5875
+ /**
5876
+ * True when a table opts into the observation/changelog substrate
5877
+ * (`def.changelog`). Callers that want to bypass the high-level {@link delete}
5878
+ * with a transaction-scoped raw delete use this to know whether the table also
5879
+ * needs the changelog / write-hook / embedding side effects that only
5880
+ * `delete()` performs — so they can keep the high-level path for such tables.
5881
+ */
5882
+ isChangelogTracked(table) {
5883
+ return this._changelogTables.has(table);
5884
+ }
5885
+ /**
5886
+ * True when this connection opened as a scoped cloud MEMBER (see
5887
+ * {@link _cloudMemberOpen}). Callers use it to route DDL-bearing work through
5888
+ * the owner-side SECURITY DEFINER helpers rather than issuing DDL the member's
5889
+ * role can't run (e.g. {@link addColumn} regenerates the masking view inside
5890
+ * `lattice_member_add_column`, so the caller must not also try to regenerate it).
5891
+ */
5892
+ isCloudMemberOpen() {
5893
+ return this._cloudMemberOpen;
5894
+ }
5662
5895
  /**
5663
5896
  * Return the normalised primary-key column list for a registered
5664
5897
  * table. Falls back to `['id']` for tables registered via raw DDL
@@ -5735,7 +5968,15 @@ var init_lattice = __esm({
5735
5968
  assertSafeIdentifier(column, "column");
5736
5969
  const existing = await introspectColumnsAsyncOrSync(this._adapter, table);
5737
5970
  if (!existing.includes(column)) {
5738
- await addColumnAsyncOrSync(this._adapter, table, column, typeSpec);
5971
+ if (this._cloudMemberOpen) {
5972
+ await runAsyncOrSync(this._adapter, `SELECT lattice_member_add_column(?, ?, ?)`, [
5973
+ table,
5974
+ column,
5975
+ typeSpec
5976
+ ]);
5977
+ } else {
5978
+ await addColumnAsyncOrSync(this._adapter, table, column, typeSpec);
5979
+ }
5739
5980
  }
5740
5981
  const cols = await introspectColumnsAsyncOrSync(this._adapter, table);
5741
5982
  this._columnCache.set(table, new Set(cols));
@@ -6667,12 +6908,39 @@ var init_lattice = __esm({
6667
6908
  async renderInBackground(outputDir, opts = {}) {
6668
6909
  const notInit = this._notInitError();
6669
6910
  if (notInit) return notInit;
6911
+ if (opts.gateOnOpen && !opts.changedTables) {
6912
+ const start = Date.now();
6913
+ const recorded = readManifest(outputDir);
6914
+ if (recorded != null) {
6915
+ const live = await computeRenderCursor(this._adapter);
6916
+ if (cursorIsFresh(recorded, live)) {
6917
+ opts.onProgress?.({
6918
+ kind: "done",
6919
+ table: null,
6920
+ entitiesRendered: 0,
6921
+ entitiesTotal: 0,
6922
+ tableIndex: 0,
6923
+ tableCount: 0,
6924
+ pct: 100,
6925
+ durationMs: Date.now() - start
6926
+ });
6927
+ const skipped = {
6928
+ filesWritten: [],
6929
+ filesSkipped: 0,
6930
+ durationMs: Date.now() - start
6931
+ };
6932
+ for (const h6 of this._renderHandlers) h6(skipped);
6933
+ return skipped;
6934
+ }
6935
+ }
6936
+ }
6670
6937
  if (!opts.changedTables) {
6671
6938
  this._pendingRenderAll = false;
6672
6939
  this._pendingRenderTables = /* @__PURE__ */ new Set();
6673
6940
  this._autoRenderPending = false;
6674
6941
  }
6675
- return this._renderGuarded(outputDir, opts);
6942
+ const { gateOnOpen: _gateOnOpen, ...engineOpts } = opts;
6943
+ return this._renderGuarded(outputDir, engineOpts);
6676
6944
  }
6677
6945
  /**
6678
6946
  * Install a per-viewer read-relation resolver for ALL renders (initial,
@@ -47863,6 +48131,111 @@ LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
47863
48131
  AND g."pk" = ANY(p_pks)
47864
48132
  AND o."owner_role" = session_user;
47865
48133
  $fn$;
48134
+
48135
+ -- Add a column to a user table AS THE OWNER, on behalf of a scoped member. A
48136
+ -- member's role has no CREATE/ALTER on the schema (the bootstrap REVOKEs CREATE
48137
+ -- from PUBLIC), so a member's GUI "add a field" write (createRow/updateRow with a
48138
+ -- field the table lacks) cannot run ALTER TABLE itself. This SECURITY DEFINER
48139
+ -- helper performs that ALTER \u2014 and the masking-view regen \u2014 with the owner's
48140
+ -- rights, so member-added columns behave identically to owner-added ones.
48141
+ --
48142
+ -- Injection-safe + minimal: p_table must be an existing BASE table in the current
48143
+ -- schema (rejected otherwise); p_type is whitelisted against the exact set the
48144
+ -- library's addColumn emits for an auto-added column (TEXT / INTEGER / REAL, plus
48145
+ -- BOOLEAN) \u2014 never interpolated raw; both identifiers go through %I (quote_ident).
48146
+ -- Member-callable (granted EXECUTE to the member group), but it can only widen the
48147
+ -- schema, never read or alter another member's data.
48148
+ CREATE OR REPLACE FUNCTION lattice_member_add_column(p_table text, p_column text, p_type text)
48149
+ RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
48150
+ DECLARE
48151
+ v_type text;
48152
+ v_view text := p_table || '_v';
48153
+ v_has_view boolean;
48154
+ v_pk_expr text;
48155
+ v_select text;
48156
+ BEGIN
48157
+ -- Never alter internal bookkeeping tables (names start with "_"). The GUI only
48158
+ -- ever calls this for a user entity table; rejecting the rest is defense-in-depth
48159
+ -- against a member invoking the function directly against ownership/audit/policy
48160
+ -- tables.
48161
+ IF left(p_table, 1) = '_' THEN
48162
+ RAISE EXCEPTION 'lattice: cannot add a column to internal table "%"', p_table;
48163
+ END IF;
48164
+
48165
+ -- p_table must be a real base table in THIS schema (search_path is pinned to the
48166
+ -- cloud schema by pinDefinerSearchPath, so to_regclass resolves there).
48167
+ IF NOT EXISTS (
48168
+ SELECT 1 FROM pg_class c
48169
+ JOIN pg_namespace n ON n.oid = c.relnamespace
48170
+ WHERE n.nspname = current_schema() AND c.relname = p_table AND c.relkind = 'r'
48171
+ ) THEN
48172
+ RAISE EXCEPTION 'lattice: no such table "%"', p_table;
48173
+ END IF;
48174
+
48175
+ -- Whitelist the column type. These are exactly the specs addColumn's
48176
+ -- inferColumnType produces (TEXT / INTEGER / REAL); BOOLEAN is allowed too.
48177
+ -- Anything else is rejected \u2014 the type is spliced as %s (NOT %I), so it must be
48178
+ -- a known-safe literal and never caller-controlled SQL.
48179
+ v_type := upper(btrim(p_type));
48180
+ IF v_type NOT IN ('TEXT', 'INTEGER', 'REAL', 'BOOLEAN') THEN
48181
+ RAISE EXCEPTION 'lattice: unsupported column type "%"', p_type;
48182
+ END IF;
48183
+
48184
+ EXECUTE format('ALTER TABLE %I ADD COLUMN IF NOT EXISTS %I %s', p_table, p_column, v_type);
48185
+
48186
+ -- If the table is cell-masked (a "<table>_v" view exists, because some column has
48187
+ -- an audience), the view selects an explicit column list \u2014 so a new column is
48188
+ -- invisible to members until the view is regenerated. Rebuild it the same way the
48189
+ -- owner path (audienceViewSql / regenerateAudienceViewFromDb) does: pass every
48190
+ -- column through except those with an 'owner' audience in __lattice_column_policy
48191
+ -- (CASE WHEN lattice_is_owner(...) THEN col END), re-apply row visibility with
48192
+ -- WHERE lattice_row_visible(table, pk), and keep the member SELECT grant on the
48193
+ -- view. Unmasked tables need no regen \u2014 the member group's table-level base grant
48194
+ -- already covers the new column.
48195
+ SELECT EXISTS (
48196
+ SELECT 1 FROM pg_class c
48197
+ JOIN pg_namespace n ON n.oid = c.relnamespace
48198
+ WHERE n.nspname = current_schema() AND c.relname = v_view AND c.relkind = 'v'
48199
+ ) INTO v_has_view;
48200
+
48201
+ IF v_has_view THEN
48202
+ -- Canonical pk expression: CAST("col" AS TEXT) joined by TAB (chr(9)) \u2014 the
48203
+ -- same serialization the RLS policies + audienceViewSql use.
48204
+ SELECT string_agg(format('CAST(%I AS TEXT)', a.attname), ' || chr(9) || '
48205
+ ORDER BY array_position(i.indkey, a.attnum))
48206
+ INTO v_pk_expr
48207
+ FROM pg_index i
48208
+ JOIN pg_class c ON c.oid = i.indrelid
48209
+ JOIN pg_namespace n ON n.oid = c.relnamespace
48210
+ JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(i.indkey)
48211
+ WHERE n.nspname = current_schema() AND c.relname = p_table AND i.indisprimary;
48212
+ IF v_pk_expr IS NULL THEN
48213
+ RAISE EXCEPTION 'lattice: cannot regenerate mask view for "%": no primary key', p_table;
48214
+ END IF;
48215
+
48216
+ -- Build the masked SELECT list in column order, applying the per-column policy.
48217
+ SELECT string_agg(
48218
+ CASE
48219
+ WHEN cp."audience" = 'owner'
48220
+ THEN format('CASE WHEN lattice_is_owner(%L, %s) THEN %I END AS %I',
48221
+ p_table, v_pk_expr, cols.column_name, cols.column_name)
48222
+ ELSE format('%I', cols.column_name)
48223
+ END,
48224
+ ', ' ORDER BY cols.ordinal_position)
48225
+ INTO v_select
48226
+ FROM information_schema.columns cols
48227
+ LEFT JOIN "__lattice_column_policy" cp
48228
+ ON cp."table_name" = p_table AND cp."column_name" = cols.column_name
48229
+ AND cp."audience" NOT IN ('', 'everyone', 'row-audience')
48230
+ WHERE cols.table_schema = current_schema() AND cols.table_name = p_table;
48231
+
48232
+ EXECUTE format(
48233
+ 'CREATE OR REPLACE VIEW %I AS SELECT %s FROM %I WHERE lattice_row_visible(%L, %s)',
48234
+ v_view, v_select, p_table, p_table, v_pk_expr);
48235
+ EXECUTE format('GRANT SELECT ON %I TO ${MEMBER_GROUP}', v_view);
48236
+ END IF;
48237
+ END $fn$;
48238
+ GRANT EXECUTE ON FUNCTION lattice_member_add_column(text, text, text) TO ${MEMBER_GROUP};
47866
48239
  `;
47867
48240
  }
47868
48241
  });
@@ -47973,6 +48346,11 @@ async function revokeRow(db, table, pk, grantee) {
47973
48346
  assertPg(db);
47974
48347
  await runAsyncOrSync(db.adapter, `SELECT lattice_revoke_row(?, ?, ?)`, [table, pk, grantee]);
47975
48348
  }
48349
+ async function batchRowGrants(db, table, pk, grant, revoke) {
48350
+ assertPg(db);
48351
+ for (const grantee of grant) await grantRow(db, table, pk, grantee);
48352
+ for (const grantee of revoke) await revokeRow(db, table, pk, grantee);
48353
+ }
47976
48354
  async function revokeMemberRole(db, role) {
47977
48355
  assertPg(db);
47978
48356
  if (!ROLE_RE.test(role)) throw new Error(`lattice: invalid member role name "${role}"`);
@@ -49075,18 +49453,9 @@ function sessionUndoneFilters(undone, sessionId) {
49075
49453
  if (sessionId) filters.push({ col: "session_id", op: "eq", val: sessionId });
49076
49454
  return filters;
49077
49455
  }
49078
- async function appendAudit(db, feed, table, rowId, op, before, after, source = "gui", sessionId, editTs) {
49079
- const undone = await db.query("_lattice_gui_audit", {
49080
- filters: sessionUndoneFilters(1, sessionId)
49081
- });
49082
- for (const r6 of undone) await db.delete("_lattice_gui_audit", r6.id);
49083
- await db.insert("_lattice_gui_audit", {
49456
+ function buildAuditRow(table, rowId, op, before, after, sessionId, editTs) {
49457
+ return {
49084
49458
  id: crypto.randomUUID(),
49085
- // Set ts explicitly (don't rely on the column DEFAULT — it uses the
49086
- // SQLite-only `strftime(...)`, which doesn't yield a parseable ISO string
49087
- // on Postgres, so cloud history rendered "Invalid Date"). #4.6 — honor the
49088
- // originating client's validated edit time when present (an offline edit
49089
- // replayed later records when it was MADE, not when it synced), else now().
49090
49459
  ts: sanitizeEditTs(editTs) ?? (/* @__PURE__ */ new Date()).toISOString(),
49091
49460
  table_name: table,
49092
49461
  row_id: rowId,
@@ -49095,7 +49464,9 @@ async function appendAudit(db, feed, table, rowId, op, before, after, source = "
49095
49464
  after_json: after ? JSON.stringify(after) : null,
49096
49465
  undone: 0,
49097
49466
  session_id: sessionId ?? null
49098
- });
49467
+ };
49468
+ }
49469
+ function publishMutationFeed(feed, table, rowId, op, before, after, source) {
49099
49470
  const labelRow = op === "delete" ? before : after;
49100
49471
  feed.publish({
49101
49472
  table,
@@ -49105,17 +49476,28 @@ async function appendAudit(db, feed, table, rowId, op, before, after, source = "
49105
49476
  summary: feedSummary(op, table, labelRow)
49106
49477
  });
49107
49478
  }
49108
- function isSchemaOp(operation2) {
49109
- return operation2.startsWith(SCHEMA_OP_PREFIX);
49110
- }
49111
- async function recordSchemaAudit(db, feed, table, operation2, before, after, summary, source = "gui", sessionId) {
49479
+ async function purgeRedoStack(db, sessionId) {
49112
49480
  const undone = await db.query("_lattice_gui_audit", {
49113
49481
  filters: sessionUndoneFilters(1, sessionId)
49114
49482
  });
49115
49483
  for (const r6 of undone) await db.delete("_lattice_gui_audit", r6.id);
49484
+ }
49485
+ async function appendAudit(db, feed, table, rowId, op, before, after, source = "gui", sessionId, editTs) {
49486
+ await purgeRedoStack(db, sessionId);
49487
+ await db.insert(
49488
+ "_lattice_gui_audit",
49489
+ buildAuditRow(table, rowId, op, before, after, sessionId, editTs)
49490
+ );
49491
+ publishMutationFeed(feed, table, rowId, op, before, after, source);
49492
+ }
49493
+ function isSchemaOp(operation2) {
49494
+ return operation2.startsWith(SCHEMA_OP_PREFIX);
49495
+ }
49496
+ async function recordSchemaAudit(db, feed, table, operation2, before, after, summary, source = "gui", sessionId) {
49497
+ await purgeRedoStack(db, sessionId);
49116
49498
  await db.insert("_lattice_gui_audit", {
49117
49499
  id: crypto.randomUUID(),
49118
- // Explicit ISO ts — see appendAudit (the SQLite-only strftime DEFAULT
49500
+ // Explicit ISO ts — see buildAuditRow (the SQLite-only strftime DEFAULT
49119
49501
  // rendered "Invalid Date" on the Postgres/cloud path).
49120
49502
  ts: (/* @__PURE__ */ new Date()).toISOString(),
49121
49503
  table_name: table,
@@ -49150,7 +49532,7 @@ async function ensureColumns(db, table, values) {
49150
49532
  const added = Object.keys(values).filter((k6) => !(k6 in existing));
49151
49533
  if (added.length === 0) return [];
49152
49534
  for (const col of added) await db.addColumn(table, col, inferColumnType(values[col]));
49153
- if (db.getDialect() === "postgres" && await cloudRlsInstalled(db)) {
49535
+ if (!db.isCloudMemberOpen() && db.getDialect() === "postgres" && await cloudRlsInstalled(db)) {
49154
49536
  const cols = db.getRegisteredColumns(table);
49155
49537
  const pk = db.getPrimaryKey(table);
49156
49538
  if (cols && pk.length > 0) await regenerateAudienceViewFromDb(db, table, Object.keys(cols), pk);
@@ -49272,7 +49654,14 @@ async function deleteRow(ctx, table, id, hard) {
49272
49654
  ctx.clientTs
49273
49655
  );
49274
49656
  } else {
49275
- await ctx.db.delete(table, id);
49657
+ await hardDelete(ctx, table, id, before);
49658
+ }
49659
+ }
49660
+ async function hardDelete(ctx, table, id, before) {
49661
+ const withClient = ctx.db.adapter.withClient?.bind(ctx.db.adapter);
49662
+ const pkCols = ctx.db.getPrimaryKey(table);
49663
+ const pkCol = pkCols.length === 1 ? pkCols[0] : void 0;
49664
+ if (!withClient || ctx.db.isChangelogTracked(table) || pkCol === void 0) {
49276
49665
  await appendAudit(
49277
49666
  ctx.db,
49278
49667
  ctx.feed,
@@ -49285,10 +49674,30 @@ async function deleteRow(ctx, table, id, hard) {
49285
49674
  ctx.sessionId,
49286
49675
  ctx.clientTs
49287
49676
  );
49677
+ await ctx.db.delete(table, id);
49678
+ return;
49288
49679
  }
49680
+ const auditRow = buildAuditRow(table, id, "delete", before, null, ctx.sessionId, ctx.clientTs);
49681
+ await purgeRedoStack(ctx.db, ctx.sessionId);
49682
+ const auditCols = AUDIT_COLUMNS.map((c6) => `"${c6}"`).join(", ");
49683
+ const auditPlaceholders = AUDIT_COLUMNS.map(() => "?").join(", ");
49684
+ const auditValues = AUDIT_COLUMNS.map((c6) => auditRow[c6]);
49685
+ const pkColQuoted = pkCol.replace(/"/g, '""');
49686
+ await withClient(async (tx) => {
49687
+ await tx.run(
49688
+ `INSERT INTO "_lattice_gui_audit" (${auditCols}) VALUES (${auditPlaceholders})`,
49689
+ auditValues
49690
+ );
49691
+ await tx.run(`DELETE FROM "${table.replace(/"/g, '""')}" WHERE "${pkColQuoted}" = ?`, [id]);
49692
+ });
49693
+ publishMutationFeed(ctx.feed, table, id, "delete", before, null, ctx.source);
49289
49694
  }
49290
- async function linkRows(ctx, table, body) {
49291
- await ctx.db.link(table, body);
49695
+ async function linkRows(ctx, table, body, forceVisibility) {
49696
+ if (forceVisibility !== void 0) {
49697
+ await ctx.db.insertForcingVisibility(table, body, forceVisibility);
49698
+ } else {
49699
+ await ctx.db.link(table, body);
49700
+ }
49292
49701
  await appendAudit(ctx.db, ctx.feed, table, null, "link", null, body, ctx.source, ctx.sessionId);
49293
49702
  }
49294
49703
  async function unlinkRows(ctx, table, body) {
@@ -49426,12 +49835,23 @@ async function revertEntry(ctx, id) {
49426
49835
  });
49427
49836
  return { ok: true, entry };
49428
49837
  }
49429
- var SCHEMA_OP_PREFIX;
49838
+ var AUDIT_COLUMNS, SCHEMA_OP_PREFIX;
49430
49839
  var init_mutations = __esm({
49431
49840
  "src/gui/mutations.ts"() {
49432
49841
  "use strict";
49433
49842
  init_cloud_connect();
49434
49843
  init_audience();
49844
+ AUDIT_COLUMNS = [
49845
+ "id",
49846
+ "ts",
49847
+ "table_name",
49848
+ "row_id",
49849
+ "operation",
49850
+ "before_json",
49851
+ "after_json",
49852
+ "undone",
49853
+ "session_id"
49854
+ ];
49435
49855
  SCHEMA_OP_PREFIX = "schema.";
49436
49856
  }
49437
49857
  });
@@ -49718,6 +50138,10 @@ async function readMachineCredential(db, kind) {
49718
50138
  }
49719
50139
  return null;
49720
50140
  }
50141
+ async function resolveAnthropicKey(db) {
50142
+ if (isAssistantCredentialCleared(CREDENTIALS.anthropic.kind)) return null;
50143
+ return await readMachineCredential(db, CREDENTIALS.anthropic.kind) ?? process.env.ANTHROPIC_API_KEY ?? null;
50144
+ }
49721
50145
  function getAggressiveness() {
49722
50146
  const n3 = readPreferences().aggressiveness;
49723
50147
  if (!Number.isFinite(n3)) return DEFAULT_AGGRESSIVENESS;
@@ -49748,6 +50172,7 @@ async function getVoiceCredential(db) {
49748
50172
  return null;
49749
50173
  }
49750
50174
  async function hasCredential(db, name, envVar) {
50175
+ if (isAssistantCredentialCleared(CREDENTIALS[name].kind)) return false;
49751
50176
  return Boolean(await readMachineCredential(db, CREDENTIALS[name].kind)) || Boolean(process.env[envVar]);
49752
50177
  }
49753
50178
  async function resolveClaudeAuth(db) {
@@ -49770,7 +50195,7 @@ async function resolveClaudeAuth(db) {
49770
50195
  } catch {
49771
50196
  }
49772
50197
  }
49773
- const apiKey = await readMachineCredential(db, CREDENTIALS.anthropic.kind) ?? process.env.ANTHROPIC_API_KEY ?? null;
50198
+ const apiKey = await resolveAnthropicKey(db);
49774
50199
  return apiKey ? { apiKey } : null;
49775
50200
  }
49776
50201
  async function hasClaudeAuth(db) {
@@ -49867,6 +50292,7 @@ async function dispatchAssistantRoute(req, res, ctx) {
49867
50292
  }
49868
50293
  const cred = CREDENTIALS[name];
49869
50294
  setAssistantCredential(cred.kind, key);
50295
+ clearAssistantCredentialCleared(cred.kind);
49870
50296
  if (db) {
49871
50297
  for (const row of await liveSecretsOfKind(db, cred.kind)) {
49872
50298
  await db.update("secrets", row.id, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() });
@@ -49883,6 +50309,7 @@ async function dispatchAssistantRoute(req, res, ctx) {
49883
50309
  return true;
49884
50310
  }
49885
50311
  deleteAssistantCredential(CREDENTIALS[name].kind);
50312
+ setAssistantCredentialCleared(CREDENTIALS[name].kind);
49886
50313
  if (db) {
49887
50314
  for (const row of await liveSecretsOfKind(db, CREDENTIALS[name].kind)) {
49888
50315
  await db.update("secrets", row.id, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() });
@@ -52072,7 +52499,7 @@ function buildSchema(db) {
52072
52499
  }
52073
52500
  return out;
52074
52501
  }
52075
- async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptions, createJunction, aggressiveness = DEFAULT_AGGRESSIVENESS, createEntity, untrusted = false) {
52502
+ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptions, createJunction, aggressiveness = DEFAULT_AGGRESSIVENESS, createEntity, untrusted = false, privateMode = false) {
52076
52503
  if (!text.trim()) return [];
52077
52504
  const auth = await resolveClaudeAuth(db);
52078
52505
  if (!auth) {
@@ -52094,6 +52521,7 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
52094
52521
  });
52095
52522
  return [];
52096
52523
  }
52524
+ const forceVis = privateMode ? "private" : void 0;
52097
52525
  const temperature = aggressivenessToTemperature(aggressiveness);
52098
52526
  let description = "";
52099
52527
  try {
@@ -52136,11 +52564,16 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
52136
52564
  }
52137
52565
  if (jx) {
52138
52566
  try {
52139
- await linkRows(mctx, jx.junction, {
52140
- id: crypto.randomUUID(),
52141
- [jx.fileFk]: fileId,
52142
- [jx.otherFk]: m4.id
52143
- });
52567
+ await linkRows(
52568
+ mctx,
52569
+ jx.junction,
52570
+ {
52571
+ id: crypto.randomUUID(),
52572
+ [jx.fileFk]: fileId,
52573
+ [jx.otherFk]: m4.id
52574
+ },
52575
+ forceVis
52576
+ );
52144
52577
  linkedCount++;
52145
52578
  if (created) {
52146
52579
  mctx.feed.publish({
@@ -52199,16 +52632,21 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
52199
52632
  if ("name" in cols && row.name == null) row.name = obj2.label;
52200
52633
  if ("title" in cols && row.title == null) row.title = obj2.label;
52201
52634
  try {
52202
- const { id: rowId } = await createRow(mctx, entity, row);
52635
+ const { id: rowId } = await createRow(mctx, entity, row, forceVis);
52203
52636
  createdCount++;
52204
52637
  const ent = entity;
52205
52638
  const jx = junctions.find((j6) => j6.otherTable === ent) ?? (createJunction ? await createJunction(ent) : null);
52206
52639
  if (jx) {
52207
- await linkRows(mctx, jx.junction, {
52208
- id: crypto.randomUUID(),
52209
- [jx.fileFk]: fileId,
52210
- [jx.otherFk]: rowId
52211
- });
52640
+ await linkRows(
52641
+ mctx,
52642
+ jx.junction,
52643
+ {
52644
+ id: crypto.randomUUID(),
52645
+ [jx.fileFk]: fileId,
52646
+ [jx.otherFk]: rowId
52647
+ },
52648
+ forceVis
52649
+ );
52212
52650
  }
52213
52651
  } catch (e6) {
52214
52652
  console.warn(`[ingest] create ${entity} from document failed:`, e6.message);
@@ -52222,12 +52660,17 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
52222
52660
  try {
52223
52661
  const title = name.replace(/\.[^./\\]+$/, "").trim() || "Note";
52224
52662
  const body = description.length > 0 ? description : text.slice(0, 2e3);
52225
- const { id: noteId } = await createRow(mctx, "notes", {
52226
- id: crypto.randomUUID(),
52227
- title,
52228
- body,
52229
- source_file_id: fileId
52230
- });
52663
+ const { id: noteId } = await createRow(
52664
+ mctx,
52665
+ "notes",
52666
+ {
52667
+ id: crypto.randomUUID(),
52668
+ title,
52669
+ body,
52670
+ source_file_id: fileId
52671
+ },
52672
+ forceVis
52673
+ );
52231
52674
  mctx.feed.publish({
52232
52675
  table: "notes",
52233
52676
  op: "insert",
@@ -52341,7 +52784,8 @@ async function ingestUrlAsFile(ctx, rawUrl, opts = {}) {
52341
52784
  ctx.enrich.createJunction,
52342
52785
  ctx.enrich.aggressiveness,
52343
52786
  ctx.enrich.createEntity,
52344
- true
52787
+ true,
52788
+ ctx.privateMode === true
52345
52789
  );
52346
52790
  }
52347
52791
  return {
@@ -53220,13 +53664,22 @@ function loadSdk() {
53220
53664
  throw new Error("Could not resolve the Anthropic constructor from '@anthropic-ai/sdk'");
53221
53665
  return ctor;
53222
53666
  }
53223
- function createAnthropicClient(auth) {
53224
- const Anthropic = loadSdk();
53667
+ function buildAnthropicConfig(auth) {
53225
53668
  const config = {};
53226
- if (auth.authToken) config.authToken = auth.authToken;
53227
- else if (auth.apiKey) config.apiKey = auth.apiKey;
53669
+ if (auth.authToken) {
53670
+ config.authToken = auth.authToken;
53671
+ config.apiKey = null;
53672
+ } else if (auth.apiKey) {
53673
+ config.apiKey = auth.apiKey;
53674
+ } else {
53675
+ config.apiKey = null;
53676
+ }
53228
53677
  if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
53229
- const sdk = new Anthropic(config);
53678
+ return config;
53679
+ }
53680
+ function createAnthropicClient(auth) {
53681
+ const Anthropic = loadSdk();
53682
+ const sdk = new Anthropic(buildAnthropicConfig(auth));
53230
53683
  return {
53231
53684
  async runTurn(params) {
53232
53685
  const stream = sdk.messages.stream({
@@ -54488,8 +54941,14 @@ var MEMBER_READABLE_BOOKKEEPING = [
54488
54941
  },
54489
54942
  {
54490
54943
  name: "_lattice_gui_audit",
54491
- privs: "SELECT, INSERT",
54492
- why: "GUI undo/redo + version history; RLS (enableGuiAuditRls) scopes reads to entries whose underlying row the member can see"
54944
+ // UPDATE + DELETE are needed by undo/redo/revert (flips an entry's `undone`)
54945
+ // and the redo-stack purge on a new mutation (deletes the session's undone
54946
+ // entries). Safe because enableGuiAuditRls installs per-op UPDATE and DELETE
54947
+ // policies whose USING is `row_id IS NULL OR lattice_row_visible(table_name,
54948
+ // row_id)` — so a member can only update/delete audit rows for entities it can
54949
+ // already see (or schema-level entries that carry no row data).
54950
+ privs: "SELECT, INSERT, UPDATE, DELETE",
54951
+ why: "GUI undo/redo/revert + redo-stack purge + version history; RLS (enableGuiAuditRls) scopes every op to entries whose underlying row the member can see"
54493
54952
  },
54494
54953
  {
54495
54954
  name: "__lattice_user_identity",
@@ -54890,6 +55349,19 @@ async function normalizeImage(path2, maxBytes) {
54890
55349
  function renderJpeg(sharp, path2, quality) {
54891
55350
  return sharp(path2).rotate().resize({ width: MAX_DIM, height: MAX_DIM, fit: "inside", withoutEnlargement: true }).jpeg({ quality }).toBuffer();
54892
55351
  }
55352
+ function buildVisionAnthropicConfig(auth) {
55353
+ const config = {};
55354
+ if (auth.authToken) {
55355
+ config.authToken = auth.authToken;
55356
+ config.apiKey = null;
55357
+ } else if (auth.apiKey) {
55358
+ config.apiKey = auth.apiKey;
55359
+ } else {
55360
+ config.apiKey = null;
55361
+ }
55362
+ if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
55363
+ return config;
55364
+ }
54893
55365
  function defaultSender(auth) {
54894
55366
  return async (input) => {
54895
55367
  const importMetaUrl = import.meta.url;
@@ -54897,11 +55369,7 @@ function defaultSender(auth) {
54897
55369
  const sdk = req("@anthropic-ai/sdk");
54898
55370
  const Anthropic = sdk.Anthropic ?? sdk.default;
54899
55371
  if (!Anthropic) throw new Error("Could not resolve Anthropic from '@anthropic-ai/sdk'");
54900
- const config = {};
54901
- if (auth.authToken) config.authToken = auth.authToken;
54902
- else if (auth.apiKey) config.apiKey = auth.apiKey;
54903
- if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
54904
- const client = new Anthropic(config);
55372
+ const client = new Anthropic(buildVisionAnthropicConfig(auth));
54905
55373
  const res = await client.messages.create({
54906
55374
  model: input.model,
54907
55375
  max_tokens: 1024,
@@ -54928,11 +55396,7 @@ function defaultPdfSender(auth) {
54928
55396
  const sdk = req("@anthropic-ai/sdk");
54929
55397
  const Anthropic = sdk.Anthropic ?? sdk.default;
54930
55398
  if (!Anthropic) throw new Error("Could not resolve Anthropic from '@anthropic-ai/sdk'");
54931
- const config = {};
54932
- if (auth.authToken) config.authToken = auth.authToken;
54933
- else if (auth.apiKey) config.apiKey = auth.apiKey;
54934
- if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
54935
- const client = new Anthropic(config);
55399
+ const client = new Anthropic(buildVisionAnthropicConfig(auth));
54936
55400
  const res = await client.messages.create({
54937
55401
  model: input.model,
54938
55402
  max_tokens: 4096,
@@ -55880,6 +56344,8 @@ var css = `
55880
56344
  .grants-panel .grants-title { font-weight: 600; margin-bottom: 6px; }
55881
56345
  .grants-panel .grants-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; cursor: pointer; }
55882
56346
  .grants-panel .grants-row input { accent-color: var(--accent); }
56347
+ .grants-panel .grants-actions { display: flex; align-items: center; gap: 8px; margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border); }
56348
+ .grants-panel .grants-dirty { font-size: 12px; }
55883
56349
 
55884
56350
  /* Inline create-row at the bottom of every table */
55885
56351
  tr.create-row td { background: var(--surface-2); }
@@ -58346,6 +58812,15 @@ var appJs = `
58346
58812
  // Per-table view state: 'live' (default) or 'trash' (soft-deleted rows).
58347
58813
  var tableViewMode = {};
58348
58814
 
58815
+ // The (table, pk) of the per-row "Manage access" grants panel that is
58816
+ // currently open, or null when none is. A soft re-render (a concurrent edit
58817
+ // by another client fires pg_notify \u2192 realtime refresh \u2192 renderRoute({soft})
58818
+ // \u2192 renderDetail/renderFsItem repaint) would otherwise re-create the detail
58819
+ // view with the panel collapsed, dropping a staged multi-select mid-edit.
58820
+ // wireRowSharing reads this after each repaint and re-opens + re-populates the
58821
+ // panel WITHOUT any network call, so the staged selection survives.
58822
+ var openGrantsPanel = null;
58823
+
58349
58824
  function renderTable(content, tableName) {
58350
58825
  var myGen = renderGen;
58351
58826
  clearUnseen(tableName);
@@ -58824,70 +59299,151 @@ var appJs = `
58824
59299
  }).catch(function (e) { showToast('Visibility update failed: ' + e.message, {}); });
58825
59300
  });
58826
59301
  });
58827
- var detailVisManage = content.querySelector('#detail-vis-manage');
58828
- if (detailVisManage) detailVisManage.addEventListener('click', function () {
59302
+ var access = row._access || {};
59303
+
59304
+ // Render the staged member checklist + a single "Save sharing" / "Cancel"
59305
+ // into the panel. Checkbox toggles mutate ONLY the local desired map \u2014
59306
+ // NO network call per toggle (the old design auto-saved live, one POST per
59307
+ // checkbox, and each grant's pg_notify collapsed the panel). A single batch
59308
+ // request fires on Save. members is the already-fetched list; desired
59309
+ // seeds from the row's current grantees (or a caller-supplied staged map
59310
+ // when re-opening after a soft re-render).
59311
+ function populateGrantsPanel(panel, members, desired) {
59312
+ // Snapshot the CURRENT (committed) grantees so Save can diff desired-vs-
59313
+ // current into adds/removes. effectiveVisibility decides whether we're
59314
+ // actually switching INTO specific-people mode (custom-0 reads as private).
59315
+ var current = {};
59316
+ (access.grantees || []).forEach(function (g) { current[g] = true; });
59317
+ if (members.length === 0) {
59318
+ panel.innerHTML = '<div class="muted">No other members in this workspace yet.</div>';
59319
+ panel.hidden = false;
59320
+ return;
59321
+ }
59322
+ function dirtyCount() {
59323
+ var n = 0;
59324
+ members.forEach(function (m) {
59325
+ if (!!desired[m.role] !== !!current[m.role]) n++;
59326
+ });
59327
+ return n;
59328
+ }
59329
+ function render() {
59330
+ var changed = dirtyCount();
59331
+ panel.innerHTML = '<div class="grants-title">Who can see this</div>' +
59332
+ members.map(function (m) {
59333
+ var label = m.name || m.email || m.role;
59334
+ return '<label class="grants-row"><input type="checkbox" data-grant-role="' + escapeHtml(m.role) + '"' +
59335
+ (desired[m.role] ? ' checked' : '') + '> ' + escapeHtml(label) + '</label>';
59336
+ }).join('') +
59337
+ '<div class="grants-actions">' +
59338
+ '<button class="btn primary" id="grants-save"' + (changed ? '' : ' disabled') + '>Save sharing</button>' +
59339
+ '<button class="btn" id="grants-cancel">Cancel</button>' +
59340
+ '<span class="grants-dirty muted">' + (changed ? (changed === 1 ? '1 change' : changed + ' changes') : 'No changes') + '</span>' +
59341
+ '</div>';
59342
+ panel.querySelectorAll('[data-grant-role]').forEach(function (cb) {
59343
+ cb.addEventListener('change', function () {
59344
+ var role = cb.getAttribute('data-grant-role');
59345
+ if (cb.checked) desired[role] = true; else delete desired[role];
59346
+ render(); // re-render to refresh the dirty indicator + Save state
59347
+ });
59348
+ });
59349
+ var cancelBtn = panel.querySelector('#grants-cancel');
59350
+ if (cancelBtn) cancelBtn.addEventListener('click', function () { closeGrantsPanel(panel); });
59351
+ var saveBtn = panel.querySelector('#grants-save');
59352
+ if (saveBtn) saveBtn.addEventListener('click', function () {
59353
+ var toAdd = [];
59354
+ var toRemove = [];
59355
+ members.forEach(function (m) {
59356
+ var want = !!desired[m.role];
59357
+ var have = !!current[m.role];
59358
+ if (want && !have) toAdd.push(m.role);
59359
+ if (!want && have) toRemove.push(m.role);
59360
+ });
59361
+ if (toAdd.length === 0 && toRemove.length === 0) { closeGrantsPanel(panel); return; }
59362
+ // Confirm the mode change ONCE, here \u2014 only when actually switching
59363
+ // INTO specific-people mode (effective vis isn't already custom AND we
59364
+ // are adding at least one grantee). Never per checkbox.
59365
+ if (effectiveVisibility(access) !== 'custom' && toAdd.length > 0) {
59366
+ if (!confirm('Sharing this with specific people switches it off "everyone"/"private". The chosen people will be able to see it. Continue?')) return;
59367
+ }
59368
+ withBusy(saveBtn, function () {
59369
+ return fetchJson('/api/cloud/row-grants', {
59370
+ method: 'POST',
59371
+ headers: { 'content-type': 'application/json' },
59372
+ body: JSON.stringify({ table: tableName, pk: id, grant: toAdd, revoke: toRemove }),
59373
+ }).then(function () {
59374
+ // Mirror the committed state locally so the re-render's indicator
59375
+ // is correct. The first grant flips the row to custom server-side;
59376
+ // revoking the last leaves custom-0, which effectiveVisibility
59377
+ // renders as private.
59378
+ var list = [];
59379
+ members.forEach(function (m) { if (desired[m.role]) list.push(m.role); });
59380
+ access.grantees = list;
59381
+ if (list.length > 0) access.visibility = 'custom';
59382
+ openGrantsPanel = null; // a successful save closes the staging session
59383
+ invalidate(tableName);
59384
+ showToast('Sharing updated', {});
59385
+ reRender();
59386
+ }).catch(function (e) {
59387
+ // Surface loudly + leave the staged selection intact so the user
59388
+ // can retry; no silent partial-success.
59389
+ showToast('Sharing update failed: ' + e.message, {});
59390
+ });
59391
+ });
59392
+ });
59393
+ panel.hidden = false;
59394
+ }
59395
+ render();
59396
+ }
59397
+
59398
+ function closeGrantsPanel(panel) {
59399
+ if (panel) panel.hidden = true;
59400
+ openGrantsPanel = null;
59401
+ }
59402
+
59403
+ // Open (or toggle shut) the manage-access panel. Fetches the member list,
59404
+ // then stages from the row's current grantees. Opening must NOT pre-flip
59405
+ // the row to 'custom' \u2014 that left a never-shared row stuck at "custom (0)".
59406
+ function openManagePanel(triggerBtn) {
58829
59407
  var panel = content.querySelector('#grants-panel');
58830
59408
  if (!panel) return;
58831
- if (!panel.hidden) { panel.hidden = true; return; }
58832
- var access = row._access || {};
58833
- // Opening the panel must NOT pre-flip the row to 'custom' \u2014 that left a
58834
- // row the user never actually shared stuck at "custom (0)". The first
58835
- // grant flips it to custom server-side (lattice_grant_row); revoking the
58836
- // last leaves it custom-with-0-grantees, which now reads as private. So
58837
- // just load the member checklist.
58838
- var ensure = Promise.resolve();
58839
- withBusy(detailVisManage, function () {
58840
- return ensure.then(function () {
58841
- return fetchJson('/api/cloud/members');
58842
- }).then(function (d) {
59409
+ if (!panel.hidden) { closeGrantsPanel(panel); return; }
59410
+ withBusy(triggerBtn, function () {
59411
+ return fetchJson('/api/cloud/members').then(function (d) {
58843
59412
  // The grant target is a member ROLE: lattice_grant_row keys on the
58844
59413
  // role, and _access.grantees holds role names. List every member
58845
59414
  // except the owner (you don't grant the owner their own row).
58846
59415
  var members = ((d && d.members) || []).filter(function (m) { return !m.isYou && m.status !== 'owner'; });
58847
- var granted = {};
58848
- (access.grantees || []).forEach(function (g) { granted[g] = true; });
58849
- if (members.length === 0) {
58850
- panel.innerHTML = '<div class="muted">No other members in this workspace yet.</div>';
58851
- } else {
58852
- panel.innerHTML = '<div class="grants-title">Who can see this</div>' + members.map(function (m) {
58853
- var label = m.name || m.email || m.role;
58854
- return '<label class="grants-row"><input type="checkbox" data-grant-role="' + escapeHtml(m.role) + '"' +
58855
- (granted[m.role] ? ' checked' : '') + '> ' + escapeHtml(label) + '</label>';
58856
- }).join('');
58857
- }
58858
- panel.hidden = false;
58859
- panel.querySelectorAll('[data-grant-role]').forEach(function (cb) {
58860
- cb.addEventListener('change', function () {
58861
- var role = cb.getAttribute('data-grant-role');
58862
- cb.disabled = true;
58863
- fetchJson('/api/cloud/row-grant', {
58864
- method: 'POST',
58865
- headers: { 'content-type': 'application/json' },
58866
- body: JSON.stringify({ table: tableName, pk: id, grantee: role, revoke: !cb.checked }),
58867
- }).then(function () {
58868
- var list = access.grantees || (access.grantees = []);
58869
- var at = list.indexOf(role);
58870
- if (cb.checked && at === -1) list.push(role);
58871
- if (!cb.checked && at !== -1) list.splice(at, 1);
58872
- // The first grant flips the row to custom server-side; mirror
58873
- // that locally so the indicator updates. Revoking the last leaves
58874
- // visibility 'custom' but effectiveVisibility renders custom-0 as
58875
- // private, so the label flips back to "Private to you".
58876
- if (list.length > 0) access.visibility = 'custom';
58877
- var infoEl = content.querySelector('#detail-vis-info');
58878
- if (infoEl) infoEl.textContent = visInfoLabel(access);
58879
- invalidate(tableName);
58880
- }).catch(function (e) {
58881
- cb.checked = !cb.checked; // revert the failed change
58882
- showToast('Access update failed: ' + e.message, {});
58883
- }).then(function () { cb.disabled = false; });
58884
- });
58885
- });
58886
- var infoEl = content.querySelector('#detail-vis-info');
58887
- if (infoEl) infoEl.textContent = visInfoLabel(access);
59416
+ var desired = {};
59417
+ (access.grantees || []).forEach(function (g) { desired[g] = true; });
59418
+ openGrantsPanel = { table: tableName, pk: id };
59419
+ populateGrantsPanel(panel, members, desired);
58888
59420
  }).catch(function (e) { showToast('Could not load members: ' + e.message, {}); });
58889
59421
  });
59422
+ }
59423
+
59424
+ var detailVisManage = content.querySelector('#detail-vis-manage');
59425
+ if (detailVisManage) detailVisManage.addEventListener('click', function () {
59426
+ openManagePanel(detailVisManage);
58890
59427
  });
59428
+
59429
+ // Preserve an open panel across a soft re-render: if the tracked panel
59430
+ // matches the row this view just repainted, re-open it and re-populate the
59431
+ // checklist from the freshly-fetched row._access WITHOUT any network call,
59432
+ // so a concurrent edit by another client doesn't lose a staged selection.
59433
+ if (openGrantsPanel && openGrantsPanel.table === tableName && openGrantsPanel.pk === id) {
59434
+ var rpanel = content.querySelector('#grants-panel');
59435
+ if (rpanel) {
59436
+ fetchJson('/api/cloud/members').then(function (d) {
59437
+ // Only re-populate if THIS panel is still the tracked-open one (a
59438
+ // newer navigation/save may have cleared it while members loaded).
59439
+ if (!openGrantsPanel || openGrantsPanel.table !== tableName || openGrantsPanel.pk !== id) return;
59440
+ var members = ((d && d.members) || []).filter(function (m) { return !m.isYou && m.status !== 'owner'; });
59441
+ var desired = {};
59442
+ (access.grantees || []).forEach(function (g) { desired[g] = true; });
59443
+ populateGrantsPanel(rpanel, members, desired);
59444
+ }).catch(function () { /* best-effort restore; a click reopens it */ });
59445
+ }
59446
+ }
58891
59447
  }
58892
59448
  function renderDetail(content, tableName, id) {
58893
59449
  var myGen = renderGen;
@@ -63671,13 +64227,21 @@ var appJs = `
63671
64227
  }
63672
64228
  function uploadFile(file) {
63673
64229
  var done = pendingIngestItem(file.name || 'file');
64230
+ // Carry the composer's "Private mode" intent so an upload made while the
64231
+ // box is checked is stamped private at insert, instead of inheriting the
64232
+ // files-table default (which can be shared-to-everyone on a cloud). Read
64233
+ // the checkbox defensively \u2014 it may not be rendered. On a local workspace
64234
+ // the box is checked+disabled, so this is '1' there too; forced visibility
64235
+ // is a harmless no-op on the single-user SQLite path.
64236
+ var pv = document.getElementById('chat-private');
64237
+ var priv = pv && pv.checked ? '1' : '0';
63674
64238
  return fetch('/api/ingest/upload', {
63675
64239
  method: 'POST',
63676
64240
  // Percent-encode the filename: HTTP header values must be ISO-8859-1,
63677
64241
  // so a Unicode filename (emoji, smart quote, accent, em-dash) would
63678
64242
  // otherwise make fetch() throw "String contains non ISO-8859-1 code
63679
64243
  // point". The server decodeURIComponent()s it back.
63680
- headers: { 'content-type': file.type || 'application/octet-stream', 'x-filename': encodeURIComponent(file.name || 'file') },
64244
+ headers: { 'content-type': file.type || 'application/octet-stream', 'x-filename': encodeURIComponent(file.name || 'file'), 'x-lattice-private': priv },
63681
64245
  body: file,
63682
64246
  })
63683
64247
  .then(function (r) { return r.json().then(function (j) { if (!r.ok) throw new Error(j.error || ('HTTP ' + r.status)); return j; }); })
@@ -64513,7 +65077,7 @@ async function checkForUpdate(pkgName, currentVersion, opts = {}) {
64513
65077
  // src/update-context.ts
64514
65078
  init_user_config();
64515
65079
  import { execFileSync as execFileSync2 } from "child_process";
64516
- import { existsSync as existsSync19, lstatSync as lstatSync2, readFileSync as readFileSync15 } from "fs";
65080
+ import { existsSync as existsSync19, lstatSync as lstatSync2, readFileSync as readFileSync15, realpathSync } from "fs";
64517
65081
  import { dirname as dirname7, join as join24, sep as sep6 } from "path";
64518
65082
  var SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
64519
65083
  function isValidVersion(v2) {
@@ -64543,10 +65107,19 @@ function isUnderGlobalPrefix(packageRoot, execPath) {
64543
65107
  }
64544
65108
  function detectInstallContext(opts = {}) {
64545
65109
  const pkgName = opts.pkgName ?? "latticesql";
64546
- const cwd = opts.cwd ?? process.cwd();
64547
65110
  const env2 = opts.env ?? process.env;
64548
65111
  const execPath = opts.execPath ?? process.execPath;
64549
- const modulePath = opts.modulePath ?? process.argv[1] ?? cwd;
65112
+ const rawCwd = opts.cwd ?? process.cwd();
65113
+ const rawModulePath = opts.modulePath ?? process.argv[1] ?? rawCwd;
65114
+ const resolveReal = (p3) => {
65115
+ try {
65116
+ return realpathSync(p3);
65117
+ } catch {
65118
+ return p3;
65119
+ }
65120
+ };
65121
+ const modulePath = resolveReal(rawModulePath);
65122
+ const cwd = resolveReal(rawCwd);
64550
65123
  const packageRoot = findPackageRoot(dirname7(modulePath), pkgName);
64551
65124
  if (packageRoot && existsSync19(join24(packageRoot, ".git"))) {
64552
65125
  return {
@@ -66332,6 +66905,27 @@ async function dispatchDbConfigRoute(req, res, ctx) {
66332
66905
  });
66333
66906
  return true;
66334
66907
  }
66908
+ if (pathname === "/api/cloud/row-grants" && method === "POST") {
66909
+ await tryHandler(res, async () => {
66910
+ const body = await readJson(req);
66911
+ const table = typeof body.table === "string" ? body.table : "";
66912
+ const pk = typeof body.pk === "string" ? body.pk : "";
66913
+ const strList = (v2) => Array.isArray(v2) ? v2.filter((x2) => typeof x2 === "string") : [];
66914
+ const grant = strList(body.grant);
66915
+ const revoke = strList(body.revoke);
66916
+ if (!table || !pk) {
66917
+ sendJson(res, { error: "table and pk are required" }, 400);
66918
+ return;
66919
+ }
66920
+ if (ctx.db.getDialect() !== "postgres") {
66921
+ sendJson(res, { error: "Per-row sharing requires a cloud (Postgres) database" }, 400);
66922
+ return;
66923
+ }
66924
+ await batchRowGrants(ctx.db, table, pk, grant, revoke);
66925
+ sendJson(res, { ok: true, table, pk, granted: grant, revoked: revoke });
66926
+ });
66927
+ return true;
66928
+ }
66335
66929
  if (pathname === "/api/cloud/s3-config" && method === "GET") {
66336
66930
  await tryHandler(res, () => {
66337
66931
  const label = activeWorkspaceLabel(ctx.configPath);
@@ -67128,7 +67722,7 @@ function enrichContext(ctx) {
67128
67722
  ...ctx.createEntity ? { createEntity: ctx.createEntity } : {}
67129
67723
  };
67130
67724
  }
67131
- async function enrichOrFail(mctx, db, fileId, text, name, ctx, res) {
67725
+ async function enrichOrFail(mctx, db, fileId, text, name, ctx, res, privateMode) {
67132
67726
  try {
67133
67727
  return await enrichWithLlm(
67134
67728
  mctx,
@@ -67140,7 +67734,9 @@ async function enrichOrFail(mctx, db, fileId, text, name, ctx, res) {
67140
67734
  ctx.entityDescriptions,
67141
67735
  ctx.createJunction,
67142
67736
  ctx.aggressiveness,
67143
- ctx.createEntity
67737
+ ctx.createEntity,
67738
+ false,
67739
+ privateMode
67144
67740
  );
67145
67741
  } catch (e6) {
67146
67742
  const err = e6;
@@ -67219,7 +67815,9 @@ async function dispatchIngestRoute(req, res, ctx) {
67219
67815
  source: "ingest",
67220
67816
  onColumnsAdded: columnDescriptionHook(ctx.db)
67221
67817
  };
67818
+ const headerPrivate = req.headers["x-lattice-private"] === "1";
67222
67819
  if (ctx.pathname === "/api/ingest/upload") {
67820
+ const forcePrivate2 = headerPrivate;
67223
67821
  const rawName = typeof req.headers["x-filename"] === "string" && req.headers["x-filename"] || "";
67224
67822
  let name2 = "upload";
67225
67823
  if (rawName) {
@@ -67317,10 +67915,15 @@ async function dispatchIngestRoute(req, res, ctx) {
67317
67915
  ...blob ? { blob_path: blob.blob_path } : {}
67318
67916
  } : blob ? { ref_kind: "blob", blob_path: blob.blob_path } : {}
67319
67917
  };
67320
- const { id: id2 } = await createRow(mctx, "files", {
67321
- ...await requiredFileDefaults(ctx.db, name2, fileId, uploadRow),
67322
- ...uploadRow
67323
- });
67918
+ const { id: id2 } = await createRow(
67919
+ mctx,
67920
+ "files",
67921
+ {
67922
+ ...await requiredFileDefaults(ctx.db, name2, fileId, uploadRow),
67923
+ ...uploadRow
67924
+ },
67925
+ forcePrivate2 ? "private" : void 0
67926
+ );
67324
67927
  try {
67325
67928
  const dedupCtx = {
67326
67929
  db: ctx.db,
@@ -67346,7 +67949,7 @@ async function dispatchIngestRoute(req, res, ctx) {
67346
67949
  }
67347
67950
  let suggestedLinks = [];
67348
67951
  if (!result.skip) {
67349
- const links = await enrichOrFail(mctx, ctx.db, id2, result.text, name2, ctx, res);
67952
+ const links = await enrichOrFail(mctx, ctx.db, id2, result.text, name2, ctx, res, forcePrivate2);
67350
67953
  if (links === null) return true;
67351
67954
  suggestedLinks = links;
67352
67955
  }
@@ -67373,6 +67976,7 @@ async function dispatchIngestRoute(req, res, ctx) {
67373
67976
  sendJson4(res, { error: e6.message }, 400);
67374
67977
  return true;
67375
67978
  }
67979
+ const forcePrivate = headerPrivate || body.private === true;
67376
67980
  if (ctx.pathname === "/api/ingest/text") {
67377
67981
  const rawText = typeof body.text === "string" ? body.text : "";
67378
67982
  if (!rawText.trim()) {
@@ -67383,7 +67987,7 @@ async function dispatchIngestRoute(req, res, ctx) {
67383
67987
  if (sourceUrl) {
67384
67988
  try {
67385
67989
  const result = await ingestUrlAsFile(
67386
- { db: ctx.db, mctx, enrich: enrichContext(ctx) },
67990
+ { db: ctx.db, mctx, enrich: enrichContext(ctx), privateMode: forcePrivate },
67387
67991
  sourceUrl
67388
67992
  );
67389
67993
  sendJson4(
@@ -67412,11 +68016,25 @@ async function dispatchIngestRoute(req, res, ctx) {
67412
68016
  description: describe(content, mime2, title),
67413
68017
  extraction_status: "extracted"
67414
68018
  };
67415
- const { id: id2 } = await createRow(mctx, "files", {
67416
- ...await requiredFileDefaults(ctx.db, title, textFileId, textRow),
67417
- ...textRow
67418
- });
67419
- const suggestedLinks = await enrichOrFail(mctx, ctx.db, id2, content, title, ctx, res);
68019
+ const { id: id2 } = await createRow(
68020
+ mctx,
68021
+ "files",
68022
+ {
68023
+ ...await requiredFileDefaults(ctx.db, title, textFileId, textRow),
68024
+ ...textRow
68025
+ },
68026
+ forcePrivate ? "private" : void 0
68027
+ );
68028
+ const suggestedLinks = await enrichOrFail(
68029
+ mctx,
68030
+ ctx.db,
68031
+ id2,
68032
+ content,
68033
+ title,
68034
+ ctx,
68035
+ res,
68036
+ forcePrivate
68037
+ );
67420
68038
  if (suggestedLinks === null) return true;
67421
68039
  sendJson4(res, { id: id2, extraction_status: "extracted", suggestedLinks }, 201);
67422
68040
  return true;
@@ -67455,10 +68073,15 @@ async function dispatchIngestRoute(req, res, ctx) {
67455
68073
  size_bytes: size,
67456
68074
  extraction_status: "pending"
67457
68075
  };
67458
- const { id } = await createRow(mctx, "files", {
67459
- ...await requiredFileDefaults(ctx.db, name, localFileId, localRow),
67460
- ...localRow
67461
- });
68076
+ const { id } = await createRow(
68077
+ mctx,
68078
+ "files",
68079
+ {
68080
+ ...await requiredFileDefaults(ctx.db, name, localFileId, localRow),
68081
+ ...localRow
68082
+ },
68083
+ forcePrivate ? "private" : void 0
68084
+ );
67462
68085
  try {
67463
68086
  const result = await extractSource(ctx.db, abs, mime, name);
67464
68087
  await updateRow(mctx, "files", id, {
@@ -67476,7 +68099,9 @@ async function dispatchIngestRoute(req, res, ctx) {
67476
68099
  ctx.entityDescriptions,
67477
68100
  ctx.createJunction,
67478
68101
  ctx.aggressiveness,
67479
- ctx.createEntity
68102
+ ctx.createEntity,
68103
+ false,
68104
+ forcePrivate
67480
68105
  );
67481
68106
  sendJson4(
67482
68107
  res,
@@ -68163,7 +68788,7 @@ function startBackgroundRender(active) {
68163
68788
  }
68164
68789
  bus.publish(e6);
68165
68790
  };
68166
- void db.renderInBackground(active.outputDir, { signal, onProgress }).then(
68791
+ void db.renderInBackground(active.outputDir, { signal, onProgress, gateOnOpen: true }).then(
68167
68792
  () => {
68168
68793
  },
68169
68794
  (err) => {