latticesql 3.0.0 → 3.2.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/index.js CHANGED
@@ -41438,7 +41438,68 @@ function cleanupEntityContexts(outputDir, entityContexts, currentSlugsByTable, m
41438
41438
  return result;
41439
41439
  }
41440
41440
 
41441
+ // src/render/progress.ts
41442
+ var THROTTLE_WINDOW_MS = 200;
41443
+ var ProgressThrottle = class {
41444
+ cb;
41445
+ windowMs;
41446
+ /**
41447
+ * Last passthrough time, keyed per table (`event.table`, or `''` for the
41448
+ * table-less `done`/`error` lifecycle events). Per-table — not a single shared
41449
+ * clock — so when tables render CONCURRENTLY each one keeps its own ~5/sec
41450
+ * budget: a fast table can't consume the window and starve a slow table's
41451
+ * progress. `force` (table-start) resets only that table's budget.
41452
+ */
41453
+ lastEmit = /* @__PURE__ */ new Map();
41454
+ constructor(cb, windowMs = THROTTLE_WINDOW_MS) {
41455
+ this.cb = cb;
41456
+ this.windowMs = windowMs;
41457
+ }
41458
+ /**
41459
+ * Emit a `table-progress` event, but only if the window since this table's
41460
+ * last passthrough has elapsed. Dropped events are simply not delivered — the
41461
+ * next one that survives carries the latest running count.
41462
+ */
41463
+ tick(event) {
41464
+ if (!this.cb) return;
41465
+ const key = event.table ?? "";
41466
+ const now = Date.now();
41467
+ if (now - (this.lastEmit.get(key) ?? 0) < this.windowMs) return;
41468
+ this.lastEmit.set(key, now);
41469
+ this.cb(event);
41470
+ }
41471
+ /**
41472
+ * Emit a lifecycle event immediately and reset this table's throttle window.
41473
+ * Use for `table-start`, `table-done`, `done`, and `error` — none of which
41474
+ * should ever be dropped. Resetting on `table-start` gives each table a clean
41475
+ * budget.
41476
+ */
41477
+ force(event) {
41478
+ if (!this.cb) return;
41479
+ this.lastEmit.set(event.table ?? "", Date.now());
41480
+ this.cb(event);
41481
+ }
41482
+ };
41483
+
41484
+ // src/concurrency.ts
41485
+ async function mapWithConcurrency(items, limit, fn) {
41486
+ const results = new Array(items.length);
41487
+ let next = 0;
41488
+ const workerCount = Math.max(1, Math.min(limit, items.length));
41489
+ const workers = Array.from({ length: workerCount }, async () => {
41490
+ for (; ; ) {
41491
+ const i6 = next++;
41492
+ if (i6 >= items.length) break;
41493
+ results[i6] = await fn(items[i6], i6);
41494
+ }
41495
+ });
41496
+ await Promise.all(workers);
41497
+ return results;
41498
+ }
41499
+
41441
41500
  // src/render/engine.ts
41501
+ var YIELD_EVERY_ENTITIES = 200;
41502
+ var RENDER_TABLE_CONCURRENCY = 4;
41442
41503
  var NOOP_RENDER = () => "";
41443
41504
  var RenderEngine = class {
41444
41505
  _schema;
@@ -41452,11 +41513,14 @@ var RenderEngine = class {
41452
41513
  this._getTaskContext = getTaskContext ?? (() => "");
41453
41514
  this._skipEmpty = options?.skipEmpty ?? false;
41454
41515
  }
41455
- async render(outputDir) {
41516
+ async render(outputDir, opts = {}) {
41456
41517
  const start = Date.now();
41457
41518
  const filesWritten = [];
41458
41519
  const counters = { skipped: 0 };
41520
+ const signal = opts.signal;
41521
+ const throttle = new ProgressThrottle(opts.onProgress);
41459
41522
  for (const [name, def] of this._schema.getTables()) {
41523
+ if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
41460
41524
  if (this._skipEmpty && def.render === NOOP_RENDER) continue;
41461
41525
  let rows = await this._schema.queryTable(this._adapter, name);
41462
41526
  if (def.relevanceFilter) {
@@ -41498,8 +41562,18 @@ var RenderEngine = class {
41498
41562
  } else {
41499
41563
  counters.skipped++;
41500
41564
  }
41565
+ throttle.force({
41566
+ kind: "table-done",
41567
+ table: name,
41568
+ entitiesRendered: rows.length,
41569
+ entitiesTotal: rows.length,
41570
+ tableIndex: 0,
41571
+ tableCount: 0,
41572
+ pct: 100
41573
+ });
41501
41574
  }
41502
- for (const [, def] of this._schema.getMultis()) {
41575
+ for (const [name, def] of this._schema.getMultis()) {
41576
+ if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
41503
41577
  const keys = await def.keys();
41504
41578
  const tables = {};
41505
41579
  if (def.tables) {
@@ -41516,12 +41590,26 @@ var RenderEngine = class {
41516
41590
  counters.skipped++;
41517
41591
  }
41518
41592
  }
41593
+ throttle.force({
41594
+ kind: "table-done",
41595
+ table: name,
41596
+ entitiesRendered: keys.length,
41597
+ entitiesTotal: keys.length,
41598
+ tableIndex: 0,
41599
+ tableCount: 0,
41600
+ pct: 100
41601
+ });
41519
41602
  }
41520
41603
  const entityContextManifest = await this._renderEntityContexts(
41521
41604
  outputDir,
41522
41605
  filesWritten,
41523
- counters
41606
+ counters,
41607
+ throttle,
41608
+ signal
41524
41609
  );
41610
+ if (entityContextManifest === null) {
41611
+ return this._abortedResult(filesWritten, counters, start);
41612
+ }
41525
41613
  if (this._schema.getEntityContexts().size > 0) {
41526
41614
  writeManifest(outputDir, {
41527
41615
  version: 2,
@@ -41529,6 +41617,29 @@ var RenderEngine = class {
41529
41617
  entityContexts: entityContextManifest
41530
41618
  });
41531
41619
  }
41620
+ const result = {
41621
+ filesWritten,
41622
+ filesSkipped: counters.skipped,
41623
+ durationMs: Date.now() - start
41624
+ };
41625
+ throttle.force({
41626
+ kind: "done",
41627
+ table: null,
41628
+ entitiesRendered: 0,
41629
+ entitiesTotal: 0,
41630
+ tableIndex: 0,
41631
+ tableCount: 0,
41632
+ pct: 100,
41633
+ durationMs: result.durationMs
41634
+ });
41635
+ return result;
41636
+ }
41637
+ /**
41638
+ * Build the partial RenderResult to return when a render is aborted. No
41639
+ * `done` event is emitted — the caller treats abort as "discard the partial
41640
+ * tree", not as a successful completion.
41641
+ */
41642
+ _abortedResult(filesWritten, counters, start) {
41532
41643
  return {
41533
41644
  filesWritten,
41534
41645
  filesSkipped: counters.skipped,
@@ -41564,127 +41675,181 @@ var RenderEngine = class {
41564
41675
  /**
41565
41676
  * Render all entity context definitions.
41566
41677
  * Mutates `filesWritten` and `counters` in place.
41567
- * Returns manifest data for the entity contexts rendered this cycle.
41678
+ * Returns manifest data for the entity contexts rendered this cycle, or
41679
+ * `null` if the render was aborted mid-flight (the caller discards the
41680
+ * partial tree). Progress is reported through `throttle`; abort is observed
41681
+ * via `signal`.
41568
41682
  */
41569
- async _renderEntityContexts(outputDir, filesWritten, counters) {
41570
- const manifestData = {};
41683
+ async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal) {
41571
41684
  const protectedTables = /* @__PURE__ */ new Set();
41572
41685
  for (const [t8, d6] of this._schema.getEntityContexts()) {
41573
41686
  if (d6.protected) protectedTables.add(t8);
41574
41687
  }
41575
- for (const [table, def] of this._schema.getEntityContexts()) {
41576
- const entityPk = this._schema.getPrimaryKey(table)[0] ?? "id";
41577
- const allRows = await this._schema.queryTable(this._adapter, table);
41578
- const directoryRoot = def.directoryRoot ?? table;
41579
- const manifestEntry = {
41580
- directoryRoot,
41581
- ...def.index ? { indexFile: def.index.outputFile } : {},
41582
- declaredFiles: Object.keys(def.files),
41583
- protectedFiles: def.protectedFiles ?? [],
41584
- entities: {}
41585
- };
41586
- if (def.index) {
41587
- const indexPath = join4(outputDir, def.index.outputFile);
41588
- if (atomicWrite(indexPath, def.index.render(allRows))) {
41589
- filesWritten.push(indexPath);
41590
- } else {
41591
- counters.skipped++;
41592
- }
41593
- }
41594
- for (const entityRow of allRows) {
41595
- const rawSlug = def.slug(entityRow);
41596
- const slug = rawSlug.replace(/[\u00A0\u2000-\u200B\u202F\u205F\u3000]/g, " ").replace(/[\u0000-\u001F\u007F]/g, "");
41597
- if (/[^a-zA-Z0-9.\-_ @(),#&'+:;!~[\]]/.test(slug)) {
41598
- throw new Error(`Invalid slug "${slug}": contains characters outside the allowed set`);
41599
- }
41600
- const entityDir = def.directory ? join4(outputDir, def.directory(entityRow)) : join4(outputDir, directoryRoot, slug);
41601
- const resolvedDir = resolve(entityDir);
41602
- const resolvedBase = resolve(outputDir);
41603
- if (!resolvedDir.startsWith(resolvedBase + sep) && resolvedDir !== resolvedBase) {
41604
- throw new Error(`Path traversal detected: slug "${slug}" escapes output directory`);
41688
+ const entityTables = [...this._schema.getEntityContexts()];
41689
+ const tableCount = entityTables.length;
41690
+ if (signal?.aborted) return null;
41691
+ const renderedEntries = await mapWithConcurrency(
41692
+ entityTables,
41693
+ RENDER_TABLE_CONCURRENCY,
41694
+ async ([table, def], tableIndex) => {
41695
+ if (signal?.aborted) return null;
41696
+ const entityPk = this._schema.getPrimaryKey(table)[0] ?? "id";
41697
+ const allRows = await this._schema.queryTable(this._adapter, table);
41698
+ const directoryRoot = def.directoryRoot ?? table;
41699
+ const entitiesTotal = allRows.length;
41700
+ throttle.force({
41701
+ kind: "table-start",
41702
+ table,
41703
+ entitiesRendered: 0,
41704
+ entitiesTotal,
41705
+ tableIndex,
41706
+ tableCount,
41707
+ pct: 0
41708
+ });
41709
+ const manifestEntry = {
41710
+ directoryRoot,
41711
+ ...def.index ? { indexFile: def.index.outputFile } : {},
41712
+ declaredFiles: Object.keys(def.files),
41713
+ protectedFiles: def.protectedFiles ?? [],
41714
+ entities: {}
41715
+ };
41716
+ if (def.index) {
41717
+ const indexPath = join4(outputDir, def.index.outputFile);
41718
+ if (atomicWrite(indexPath, def.index.render(allRows))) {
41719
+ filesWritten.push(indexPath);
41720
+ } else {
41721
+ counters.skipped++;
41722
+ }
41605
41723
  }
41606
- mkdirSync2(entityDir, { recursive: true });
41607
- if (def.attachFileColumn) {
41608
- const filePath = entityRow[def.attachFileColumn];
41609
- if (filePath && typeof filePath === "string" && filePath.length > 0) {
41610
- if (def.attachFileMode === "reference") {
41611
- const refPath = join4(entityDir, `${basename(filePath)}.ref.md`);
41612
- try {
41613
- atomicWrite(refPath, `# Reference
41724
+ for (let i6 = 0; i6 < allRows.length; i6++) {
41725
+ const entityRow = allRows[i6];
41726
+ if (signal?.aborted) return null;
41727
+ if (i6 > 0 && i6 % YIELD_EVERY_ENTITIES === 0) {
41728
+ await new Promise((r6) => setImmediate(r6));
41729
+ }
41730
+ const rawSlug = def.slug(entityRow);
41731
+ const slug = rawSlug.replace(/[\u00A0\u2000-\u200B\u202F\u205F\u3000]/g, " ").replace(/[\u0000-\u001F\u007F]/g, "");
41732
+ if (/[^a-zA-Z0-9.\-_ @(),#&'+:;!~[\]]/.test(slug)) {
41733
+ throw new Error(`Invalid slug "${slug}": contains characters outside the allowed set`);
41734
+ }
41735
+ const entityDir = def.directory ? join4(outputDir, def.directory(entityRow)) : join4(outputDir, directoryRoot, slug);
41736
+ const resolvedDir = resolve(entityDir);
41737
+ const resolvedBase = resolve(outputDir);
41738
+ if (!resolvedDir.startsWith(resolvedBase + sep) && resolvedDir !== resolvedBase) {
41739
+ throw new Error(`Path traversal detected: slug "${slug}" escapes output directory`);
41740
+ }
41741
+ mkdirSync2(entityDir, { recursive: true });
41742
+ if (def.attachFileColumn) {
41743
+ const filePath = entityRow[def.attachFileColumn];
41744
+ if (filePath && typeof filePath === "string" && filePath.length > 0) {
41745
+ if (def.attachFileMode === "reference") {
41746
+ const refPath = join4(entityDir, `${basename(filePath)}.ref.md`);
41747
+ try {
41748
+ atomicWrite(refPath, `# Reference
41614
41749
 
41615
41750
  - **location:** ${filePath}
41616
41751
  `);
41617
- filesWritten.push(refPath);
41618
- } catch {
41619
- }
41620
- } else {
41621
- const absPath = isAbsolute(filePath) ? filePath : resolve(outputDir, filePath);
41622
- if (existsSync4(absPath)) {
41623
- const destPath = join4(entityDir, basename(absPath));
41624
- if (!existsSync4(destPath)) {
41625
- try {
41626
- copyFileSync2(absPath, destPath);
41627
- filesWritten.push(destPath);
41628
- } catch {
41752
+ filesWritten.push(refPath);
41753
+ } catch {
41754
+ }
41755
+ } else {
41756
+ const absPath = isAbsolute(filePath) ? filePath : resolve(outputDir, filePath);
41757
+ if (existsSync4(absPath)) {
41758
+ const destPath = join4(entityDir, basename(absPath));
41759
+ if (!existsSync4(destPath)) {
41760
+ try {
41761
+ copyFileSync2(absPath, destPath);
41762
+ filesWritten.push(destPath);
41763
+ } catch {
41764
+ }
41629
41765
  }
41630
41766
  }
41631
41767
  }
41632
41768
  }
41633
41769
  }
41634
- }
41635
- const renderedFiles = /* @__PURE__ */ new Map();
41636
- const entityFileHashes = {};
41637
- const protection = protectedTables.size > 0 ? { protectedTables, currentTable: table } : void 0;
41638
- for (const [filename, spec] of Object.entries(def.files)) {
41639
- const mergeDefaults = def.sourceDefaults && spec.source.type !== "self" && spec.source.type !== "custom" && spec.source.type !== "enriched";
41640
- const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
41641
- const rows = await resolveEntitySource(
41642
- source,
41643
- entityRow,
41644
- entityPk,
41645
- this._adapter,
41646
- protection
41647
- );
41648
- if (spec.omitIfEmpty && rows.length === 0) continue;
41649
- const renderFn = compileEntityRender(spec.render);
41650
- const content = truncateContent(renderFn(rows), spec.budget);
41651
- renderedFiles.set(filename, content);
41652
- entityFileHashes[filename] = { hash: contentHash(content) };
41653
- const filePath = join4(entityDir, filename);
41654
- if (atomicWrite(filePath, content)) {
41655
- filesWritten.push(filePath);
41656
- } else {
41657
- counters.skipped++;
41658
- }
41659
- }
41660
- const fileKeys = Object.keys(def.files);
41661
- const effectiveCombined = def.combined ?? (fileKeys.length > 1 && renderedFiles.size > 1 ? (
41662
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41663
- { outputFile: fileKeys[0] }
41664
- ) : void 0);
41665
- if (effectiveCombined && renderedFiles.size > 0) {
41666
- const excluded = new Set(effectiveCombined.exclude ?? []);
41667
- const parts = [];
41668
- for (const filename of Object.keys(def.files)) {
41669
- if (!excluded.has(filename) && renderedFiles.has(filename)) {
41670
- parts.push(renderedFiles.get(filename) ?? "");
41671
- }
41672
- }
41673
- if (parts.length > 0) {
41674
- const combinedContent = parts.join("\n\n---\n\n");
41675
- const combinedPath = join4(entityDir, effectiveCombined.outputFile);
41676
- if (atomicWrite(combinedPath, combinedContent)) {
41677
- filesWritten.push(combinedPath);
41770
+ const renderedFiles = /* @__PURE__ */ new Map();
41771
+ const entityFileHashes = {};
41772
+ const protection = protectedTables.size > 0 ? { protectedTables, currentTable: table } : void 0;
41773
+ for (const [filename, spec] of Object.entries(def.files)) {
41774
+ if (signal?.aborted) return null;
41775
+ const mergeDefaults = def.sourceDefaults && spec.source.type !== "self" && spec.source.type !== "custom" && spec.source.type !== "enriched";
41776
+ const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
41777
+ const rows = await resolveEntitySource(
41778
+ source,
41779
+ entityRow,
41780
+ entityPk,
41781
+ this._adapter,
41782
+ protection
41783
+ );
41784
+ if (spec.omitIfEmpty && rows.length === 0) continue;
41785
+ const renderFn = compileEntityRender(spec.render);
41786
+ const content = truncateContent(renderFn(rows), spec.budget);
41787
+ renderedFiles.set(filename, content);
41788
+ entityFileHashes[filename] = { hash: contentHash(content) };
41789
+ const filePath = join4(entityDir, filename);
41790
+ if (atomicWrite(filePath, content)) {
41791
+ filesWritten.push(filePath);
41678
41792
  } else {
41679
41793
  counters.skipped++;
41680
41794
  }
41681
- renderedFiles.set(effectiveCombined.outputFile, combinedContent);
41682
- entityFileHashes[effectiveCombined.outputFile] = { hash: contentHash(combinedContent) };
41683
41795
  }
41796
+ const fileKeys = Object.keys(def.files);
41797
+ const effectiveCombined = def.combined ?? (fileKeys.length > 1 && renderedFiles.size > 1 ? (
41798
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41799
+ { outputFile: fileKeys[0] }
41800
+ ) : void 0);
41801
+ if (effectiveCombined && renderedFiles.size > 0) {
41802
+ const excluded = new Set(effectiveCombined.exclude ?? []);
41803
+ const parts = [];
41804
+ for (const filename of Object.keys(def.files)) {
41805
+ if (!excluded.has(filename) && renderedFiles.has(filename)) {
41806
+ parts.push(renderedFiles.get(filename) ?? "");
41807
+ }
41808
+ }
41809
+ if (parts.length > 0) {
41810
+ const combinedContent = parts.join("\n\n---\n\n");
41811
+ const combinedPath = join4(entityDir, effectiveCombined.outputFile);
41812
+ if (atomicWrite(combinedPath, combinedContent)) {
41813
+ filesWritten.push(combinedPath);
41814
+ } else {
41815
+ counters.skipped++;
41816
+ }
41817
+ renderedFiles.set(effectiveCombined.outputFile, combinedContent);
41818
+ entityFileHashes[effectiveCombined.outputFile] = {
41819
+ hash: contentHash(combinedContent)
41820
+ };
41821
+ }
41822
+ }
41823
+ manifestEntry.entities[slug] = entityFileHashes;
41824
+ const entitiesRendered = i6 + 1;
41825
+ throttle.tick({
41826
+ kind: "table-progress",
41827
+ table,
41828
+ entitiesRendered,
41829
+ entitiesTotal,
41830
+ tableIndex,
41831
+ tableCount,
41832
+ pct: entitiesTotal > 0 ? entitiesRendered / entitiesTotal * 100 : 100
41833
+ });
41684
41834
  }
41685
- manifestEntry.entities[slug] = entityFileHashes;
41835
+ throttle.force({
41836
+ kind: "table-done",
41837
+ table,
41838
+ entitiesRendered: entitiesTotal,
41839
+ entitiesTotal,
41840
+ tableIndex,
41841
+ tableCount,
41842
+ pct: 100
41843
+ });
41844
+ return manifestEntry;
41686
41845
  }
41687
- manifestData[table] = manifestEntry;
41846
+ );
41847
+ if (signal?.aborted) return null;
41848
+ const manifestData = {};
41849
+ for (let i6 = 0; i6 < renderedEntries.length; i6++) {
41850
+ const entry = renderedEntries[i6];
41851
+ if (entry == null) return null;
41852
+ manifestData[entityTables[i6][0]] = entry;
41688
41853
  }
41689
41854
  return manifestData;
41690
41855
  }
@@ -42944,14 +43109,27 @@ function buildParsedConfig(raw, sourceName, configDir2) {
42944
43109
  const entityContexts = parseEntityContexts(config.entityContexts);
42945
43110
  return name !== void 0 ? { dbPath, name, tables, entityContexts } : { dbPath, tables, entityContexts };
42946
43111
  }
43112
+ function isDbRefShaped(raw) {
43113
+ return /^\s*\$\{LATTICE_DB:/.test(raw);
43114
+ }
43115
+ function parseDbRef(raw) {
43116
+ const m4 = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(raw.trim());
43117
+ return m4 ? { label: m4[1] ?? "" } : null;
43118
+ }
42947
43119
  function resolveDbPath(raw, configDir2) {
42948
- const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(raw.trim());
42949
- if (labelMatch) {
42950
- const label = labelMatch[1] ?? "";
42951
- const url = getDbCredential(label);
43120
+ if (isDbRefShaped(raw)) {
43121
+ const ref = parseDbRef(raw);
43122
+ if (!ref) {
43123
+ throw new Error(
43124
+ `Lattice: malformed \${LATTICE_DB:\u2026} reference ${JSON.stringify(
43125
+ raw.trim()
43126
+ )} \u2014 the label may contain only [A-Za-z0-9._-] (no spaces). This usually means a workspace was created with an unsanitized name.`
43127
+ );
43128
+ }
43129
+ const url = getDbCredential(ref.label);
42952
43130
  if (!url) {
42953
43131
  throw new Error(
42954
- `Lattice: config references \${LATTICE_DB:${label}} but no credential is saved for "${label}". Save one via the GUI's Database panel or set LATTICE_DB_${label}.`
43132
+ `Lattice: config references \${LATTICE_DB:${ref.label}} but no credential is saved for "${ref.label}". Save one via the GUI's Database panel or set LATTICE_DB_${ref.label}.`
42955
43133
  );
42956
43134
  }
42957
43135
  return url;
@@ -42959,6 +43137,13 @@ function resolveDbPath(raw, configDir2) {
42959
43137
  if (/^postgres(ql)?:\/\//i.test(raw) || raw.startsWith("file:") || raw === ":memory:") {
42960
43138
  return raw;
42961
43139
  }
43140
+ if (raw.includes("${")) {
43141
+ throw new Error(
43142
+ `Lattice: refusing to treat ${JSON.stringify(
43143
+ raw.trim()
43144
+ )} as a database path \u2014 it looks like a malformed variable reference, not a file path.`
43145
+ );
43146
+ }
42962
43147
  return resolve2(configDir2, raw);
42963
43148
  }
42964
43149
  var warnedDeprecatedRefs = /* @__PURE__ */ new Set();
@@ -44291,6 +44476,54 @@ var Lattice = class _Lattice {
44291
44476
  async insert(table, row, provenance) {
44292
44477
  const notInit = this._notInitError();
44293
44478
  if (notInit) return notInit;
44479
+ const { sql, values, pkValue, rowWithPk } = this._prepareInsert(table, row);
44480
+ await runAsyncOrSync(this._adapter, sql, values);
44481
+ await this._afterInsert(table, pkValue, rowWithPk, provenance);
44482
+ return pkValue;
44483
+ }
44484
+ /**
44485
+ * Insert a row while atomically forcing its cloud row-visibility, regardless of
44486
+ * the table's `default_row_visibility`. The per-table insert trigger reads a
44487
+ * transaction-local GUC (`lattice.force_row_visibility`); we set it and run the
44488
+ * INSERT inside a single transaction, so the row is stamped at `visibility` the
44489
+ * instant it exists — it is never momentarily visible at the table default, and
44490
+ * the change-feed `NOTIFY` (delivered only at COMMIT) fires when the row already
44491
+ * carries this visibility. This closes the create-then-demote window that a
44492
+ * plain `insert()` + `setRowVisibility()` would leave open.
44493
+ *
44494
+ * Postgres-only: SQLite is single-user (no cross-viewer leak) and has no trigger
44495
+ * to read the GUC, so it degrades to a plain {@link insert}. A `never_share`
44496
+ * table still wins — its rows are forced private even if `visibility` is
44497
+ * `'everyone'` (the trigger enforces that precedence).
44498
+ *
44499
+ * @since 3.1.0
44500
+ */
44501
+ async insertForcingVisibility(table, row, visibility, provenance) {
44502
+ const notInit = this._notInitError();
44503
+ if (notInit) return notInit;
44504
+ const vis = visibility;
44505
+ if (vis !== "private" && vis !== "everyone") {
44506
+ throw new Error(`lattice: invalid forced visibility "${vis}"`);
44507
+ }
44508
+ const withClient = this._adapter.withClient?.bind(this._adapter);
44509
+ if (this.getDialect() !== "postgres" || !withClient) {
44510
+ return this.insert(table, row, provenance);
44511
+ }
44512
+ const { sql, values, pkValue, rowWithPk } = this._prepareInsert(table, row);
44513
+ await withClient(async (tx) => {
44514
+ await tx.run(`SELECT set_config('lattice.force_row_visibility', ?, true)`, [visibility]);
44515
+ await tx.run(sql, values);
44516
+ });
44517
+ await this._afterInsert(table, pkValue, rowWithPk, provenance);
44518
+ return pkValue;
44519
+ }
44520
+ /**
44521
+ * Build the INSERT statement + canonical pk for a row (sanitize → schema-filter →
44522
+ * auto-pk → encrypt). Shared by {@link insert} and {@link insertForcingVisibility}
44523
+ * so both produce byte-identical writes; the latter only differs in running it
44524
+ * inside a GUC-scoped transaction.
44525
+ */
44526
+ _prepareInsert(table, row) {
44294
44527
  this._assertIdent(table);
44295
44528
  const sanitized = this._filterToSchemaColumns(table, this._sanitizer.sanitizeRow(row));
44296
44529
  const pkCols = this._schema.getPrimaryKey(table);
@@ -44306,12 +44539,17 @@ var Lattice = class _Lattice {
44306
44539
  const cols = Object.keys(encrypted).map((c6) => `"${c6}"`).join(", ");
44307
44540
  const placeholders = Object.keys(encrypted).map(() => "?").join(", ");
44308
44541
  const values = Object.values(encrypted);
44309
- await runAsyncOrSync(
44310
- this._adapter,
44311
- `INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`,
44312
- values
44313
- );
44314
44542
  const pkValue = this._serializeRowPk(table, rowWithPk);
44543
+ return {
44544
+ sql: `INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`,
44545
+ values,
44546
+ pkValue,
44547
+ rowWithPk
44548
+ };
44549
+ }
44550
+ /** Post-insert side effects (changelog, audit, write hooks, embedding sync),
44551
+ * identical for the plain and force-visibility insert paths. */
44552
+ async _afterInsert(table, pkValue, rowWithPk, provenance) {
44315
44553
  await this._appendChangelog(
44316
44554
  table,
44317
44555
  pkValue,
@@ -44325,7 +44563,6 @@ var Lattice = class _Lattice {
44325
44563
  this._sanitizer.emitAudit(table, "insert", pkValue);
44326
44564
  await this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
44327
44565
  this._syncEmbedding(table, "insert", rowWithPk, pkValue);
44328
- return pkValue;
44329
44566
  }
44330
44567
  /**
44331
44568
  * Insert a row and return the full inserted row (including auto-generated
@@ -44392,6 +44629,7 @@ var Lattice = class _Lattice {
44392
44629
  const sanitized = this._filterToSchemaColumns(table, this._sanitizer.sanitizeRow(row));
44393
44630
  const encrypted = this._encryptRow(table, sanitized);
44394
44631
  const setCols = Object.keys(encrypted).map((c6) => `"${c6}" = ?`).join(", ");
44632
+ if (setCols === "") return;
44395
44633
  const { clause, params: pkParams } = this._pkWhere(table, id);
44396
44634
  let previousValues = null;
44397
44635
  if (this._changelogTables.has(table)) {
@@ -45008,13 +45246,32 @@ var Lattice = class _Lattice {
45008
45246
  // -------------------------------------------------------------------------
45009
45247
  // Sync
45010
45248
  // -------------------------------------------------------------------------
45011
- async render(outputDir) {
45249
+ async render(outputDir, opts = {}) {
45012
45250
  const notInit = this._notInitError();
45013
45251
  if (notInit) return notInit;
45014
- const result = await this._render.render(outputDir);
45252
+ const result = await this._render.render(outputDir, opts);
45015
45253
  for (const h6 of this._renderHandlers) h6(result);
45016
45254
  return result;
45017
45255
  }
45256
+ /**
45257
+ * Render into `outputDir` through the shared single-flight guard, intended to
45258
+ * be called fire-and-forget (e.g. the GUI's instant-open background render).
45259
+ *
45260
+ * The guard ({@link _renderGuarded}) holds {@link _autoRenderInFlight} for the
45261
+ * render's duration, so a data mutation that lands while this render is in
45262
+ * flight is deferred by {@link _runAutoRender} and coalesced — when this
45263
+ * render settles, `finally` clears the flag and re-arms exactly one follow-up
45264
+ * render via {@link _rearmAutoRenderIfPending}. Net invariant: at most one
45265
+ * render to a given dir at a time.
45266
+ *
45267
+ * Errors propagate to the caller (the GUI surfaces them, never silently swallowed); they are
45268
+ * not swallowed here.
45269
+ */
45270
+ async renderInBackground(outputDir, opts = {}) {
45271
+ const notInit = this._notInitError();
45272
+ if (notInit) return notInit;
45273
+ return this._renderGuarded(outputDir, opts);
45274
+ }
45018
45275
  async sync(outputDir) {
45019
45276
  const notInit = this._notInitError();
45020
45277
  if (notInit) return notInit;
@@ -45356,6 +45613,30 @@ var Lattice = class _Lattice {
45356
45613
  }, this._autoRenderDebounceMs);
45357
45614
  this._autoRenderTimer.unref();
45358
45615
  }
45616
+ /**
45617
+ * Shared single-flight render path used by {@link renderInBackground}.
45618
+ *
45619
+ * Holds {@link _autoRenderInFlight} for the render's duration so the
45620
+ * mutation-driven {@link _runAutoRender} defers while this render runs (it
45621
+ * sees the flag and marks itself pending instead of starting a second,
45622
+ * overlapping render). On settle, `finally` clears the flag and re-arms a
45623
+ * single coalesced follow-up render if any mutation arrived mid-flight.
45624
+ * Errors propagate to the caller; the flag is always cleared.
45625
+ */
45626
+ async _renderGuarded(outputDir, opts) {
45627
+ while (this._autoRenderInFlight) {
45628
+ await new Promise((r6) => setImmediate(r6));
45629
+ }
45630
+ this._autoRenderInFlight = true;
45631
+ try {
45632
+ const result = await this._render.render(outputDir, opts);
45633
+ for (const h6 of this._renderHandlers) h6(result);
45634
+ return result;
45635
+ } finally {
45636
+ this._autoRenderInFlight = false;
45637
+ this._rearmAutoRenderIfPending();
45638
+ }
45639
+ }
45359
45640
  async _runAutoRender() {
45360
45641
  const dir = this._autoRenderDir;
45361
45642
  if (!dir || !this._initialized) return;
@@ -46916,7 +47197,7 @@ function archiveLocalSqlite(dbPath) {
46916
47197
  async function cloudRlsInstalled(probe) {
46917
47198
  const row = await getAsyncOrSync(
46918
47199
  probe.adapter,
46919
- `SELECT to_regclass('public.__lattice_owners') AS reg`
47200
+ `SELECT to_regclass('__lattice_owners') AS reg`
46920
47201
  );
46921
47202
  return !!row && row.reg != null;
46922
47203
  }
@@ -46973,6 +47254,19 @@ function isPostgresUrl(url) {
46973
47254
  }
46974
47255
 
46975
47256
  // src/cloud/rls.ts
47257
+ async function runCloudBootstrapSql(db, sql) {
47258
+ const adapter = db.adapter;
47259
+ if (adapter.withClient) {
47260
+ await adapter.withClient(async (tx) => {
47261
+ await tx.run("SELECT pg_advisory_xact_lock($1::bigint)", [
47262
+ LATTICE_MIGRATION_LOCK_ID.toString()
47263
+ ]);
47264
+ await tx.run(sql);
47265
+ });
47266
+ } else {
47267
+ await runAsyncOrSync(adapter, sql);
47268
+ }
47269
+ }
46976
47270
  function isPg(db) {
46977
47271
  return db.getDialect() === "postgres";
46978
47272
  }
@@ -46983,6 +47277,31 @@ function pkSqlExpr(pkCols, prefix) {
46983
47277
  return pkCols.map((c6) => `CAST(${prefix}"${c6}" AS TEXT)`).join(` || chr(9) || `);
46984
47278
  }
46985
47279
  var MEMBER_GROUP = "lattice_members";
47280
+ function pinDefinerSearchPath(sql, schema) {
47281
+ const safe = schema.replace(/"/g, '""');
47282
+ return sql.replace(
47283
+ /SECURITY DEFINER AS/g,
47284
+ `SECURITY DEFINER SET search_path = "${safe}", pg_temp AS`
47285
+ );
47286
+ }
47287
+ async function cloudSchema(db) {
47288
+ const row = await getAsyncOrSync(db.adapter, `SELECT current_schema() AS schema`);
47289
+ const s2 = row?.schema;
47290
+ if (typeof s2 !== "string" || s2.length === 0) {
47291
+ throw new Error("cloud RLS: could not resolve current_schema() for search_path pinning");
47292
+ }
47293
+ return s2;
47294
+ }
47295
+ function revokeSchemaCreateSql(schema) {
47296
+ const lit = `'${schema.replace(/'/g, "''")}'`;
47297
+ return `
47298
+ DO $LATTICE_REVOKE$ BEGIN
47299
+ EXECUTE format('REVOKE CREATE ON SCHEMA %I FROM PUBLIC', ${lit});
47300
+ EXCEPTION WHEN OTHERS THEN
47301
+ NULL; -- not the schema owner, or already revoked
47302
+ END $LATTICE_REVOKE$;
47303
+ `;
47304
+ }
46986
47305
  var CLOUD_RLS_BOOTSTRAP_SQL = `
46987
47306
  -- Member group (NOLOGIN). Members inherit schema/connect/table privileges from it;
46988
47307
  -- RLS filters per the individual member's login role, so the group never widens
@@ -47042,6 +47361,82 @@ CREATE TABLE IF NOT EXISTS "__lattice_cell_grants" (
47042
47361
  PRIMARY KEY ("table_name", "pk", "column_name", "grantee_role")
47043
47362
  );
47044
47363
 
47364
+ -- Per-table policy: the owner-controlled defaults that govern a whole table.
47365
+ -- default_row_visibility is the visibility NEW rows are stamped with (the insert
47366
+ -- trigger reads it); never_share is a hard exclusion \u2014 the share/grant functions
47367
+ -- refuse to elevate such a table and the trigger forces its rows private. Owner-
47368
+ -- managed; members have no grant (it never appears in their data API).
47369
+ CREATE TABLE IF NOT EXISTS "__lattice_table_policy" (
47370
+ "table_name" text PRIMARY KEY,
47371
+ "default_row_visibility" text NOT NULL DEFAULT 'private'
47372
+ CHECK ("default_row_visibility" IN ('private','everyone')),
47373
+ "never_share" boolean NOT NULL DEFAULT false,
47374
+ "updated_by" text NOT NULL DEFAULT session_user,
47375
+ "updated_at" timestamptz NOT NULL DEFAULT now()
47376
+ );
47377
+
47378
+ -- Per-column audience policy: the CANONICAL store of which column carries which
47379
+ -- audience spec (role: / subject: / source: / owner / everyone). Previously the
47380
+ -- spec lived only in the owner's on-disk YAML and was compiled into the mask view
47381
+ -- once at init; storing it here makes it cloud-canonical and member-consistent.
47382
+ -- The generated <table>_v mask view is regenerated from THIS table on change.
47383
+ -- Owner-managed; members have no grant.
47384
+ CREATE TABLE IF NOT EXISTS "__lattice_column_policy" (
47385
+ "table_name" text NOT NULL,
47386
+ "column_name" text NOT NULL,
47387
+ "audience" text NOT NULL,
47388
+ "updated_by" text NOT NULL DEFAULT session_user,
47389
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
47390
+ PRIMARY KEY ("table_name", "column_name")
47391
+ );
47392
+
47393
+ -- Owner-only audit of issued member invites: which scoped role was minted for
47394
+ -- which email (HASHED \u2014 the plaintext email is never stored), when it expires,
47395
+ -- and whether it was redeemed/revoked. No plaintext password is ever stored
47396
+ -- (the credential lives only inside the email-bound token the owner delivers).
47397
+ -- Owner-managed; members have no grant. Named distinctly from any legacy
47398
+ -- team-model invitations table so a pre-existing cloud never collides.
47399
+ CREATE TABLE IF NOT EXISTS "__lattice_member_invites" (
47400
+ "id" text PRIMARY KEY,
47401
+ "role" text NOT NULL,
47402
+ "email_hash" text NOT NULL,
47403
+ "email" text,
47404
+ "created_by" text NOT NULL DEFAULT session_user,
47405
+ "created_at" timestamptz NOT NULL DEFAULT now(),
47406
+ "expires_at" timestamptz NOT NULL,
47407
+ "redeemed_at" timestamptz,
47408
+ "revoked_at" timestamptz
47409
+ );
47410
+ -- Plaintext invitee email (owner-only table; members have no grant) so the
47411
+ -- owner's Members list can show who each member is. Added via ALTER so clouds
47412
+ -- created before this column converge to it on the owner's next open (the
47413
+ -- bootstrap is now run directly + idempotently, not version-gated).
47414
+ ALTER TABLE "__lattice_member_invites" ADD COLUMN IF NOT EXISTS "email" text;
47415
+
47416
+ -- #3.1 \u2014 one-time-use + revocation enforcement. After a member authenticates to
47417
+ -- the cloud with their minted credential, the join path calls this to CLAIM the
47418
+ -- invite. The single atomic UPDATE stamps redeemed_at and returns true ONLY when
47419
+ -- an invite for the CALLING role (session_user) is still pending: not already
47420
+ -- redeemed (one-time-use), not revoked, and not expired. A replayed redeem of a
47421
+ -- leaked token, a revoked invite, or an expired one returns false, so the caller
47422
+ -- rejects the join. Members have no direct grant on the owner-only
47423
+ -- __lattice_member_invites table \u2014 this SECURITY DEFINER function is the only
47424
+ -- path, and it can claim ONLY the caller's own invite (keyed on session_user,
47425
+ -- never a caller-supplied parameter, so one member can't burn another's invite).
47426
+ CREATE OR REPLACE FUNCTION lattice_claim_invite()
47427
+ RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER AS $fn$
47428
+ DECLARE v_ok boolean;
47429
+ BEGIN
47430
+ UPDATE "__lattice_member_invites"
47431
+ SET "redeemed_at" = now()
47432
+ WHERE "role" = session_user
47433
+ AND "redeemed_at" IS NULL
47434
+ AND "revoked_at" IS NULL
47435
+ AND "expires_at" > now()
47436
+ RETURNING true INTO v_ok;
47437
+ RETURN COALESCE(v_ok, false);
47438
+ END $fn$;
47439
+
47045
47440
  -- Visibility check. SECURITY DEFINER so it reads bookkeeping the member can't;
47046
47441
  -- keyed on session_user (the member's login role). A row with no ownership record
47047
47442
  -- is visible to nobody.
@@ -47067,6 +47462,10 @@ BEGIN
47067
47462
  IF p_visibility NOT IN ('private','everyone','custom') THEN
47068
47463
  RAISE EXCEPTION 'lattice: invalid visibility %', p_visibility;
47069
47464
  END IF;
47465
+ IF p_visibility <> 'private'
47466
+ AND COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = p_table), false) THEN
47467
+ RAISE EXCEPTION 'lattice: "%" is a private-only table and cannot be shared', p_table;
47468
+ END IF;
47070
47469
  SELECT o."owner_role" INTO v_owner FROM "__lattice_owners" o
47071
47470
  WHERE o."table_name" = p_table AND o."pk" = p_pk;
47072
47471
  IF v_owner IS NULL THEN RAISE EXCEPTION 'lattice: no ownership record for %/%', p_table, p_pk; END IF;
@@ -47080,6 +47479,9 @@ CREATE OR REPLACE FUNCTION lattice_grant_row(p_table text, p_pk text, p_grantee
47080
47479
  RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
47081
47480
  DECLARE v_owner text;
47082
47481
  BEGIN
47482
+ IF COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = p_table), false) THEN
47483
+ RAISE EXCEPTION 'lattice: "%" is a private-only table and cannot be shared', p_table;
47484
+ END IF;
47083
47485
  SELECT o."owner_role" INTO v_owner FROM "__lattice_owners" o
47084
47486
  WHERE o."table_name" = p_table AND o."pk" = p_pk;
47085
47487
  IF v_owner IS NULL THEN RAISE EXCEPTION 'lattice: no ownership record for %/%', p_table, p_pk; END IF;
@@ -47169,6 +47571,9 @@ CREATE OR REPLACE FUNCTION lattice_grant_cell(p_table text, p_pk text, p_column
47169
47571
  RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
47170
47572
  DECLARE v_owner text;
47171
47573
  BEGIN
47574
+ IF COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = p_table), false) THEN
47575
+ RAISE EXCEPTION 'lattice: "%" is a private-only table and cannot be shared', p_table;
47576
+ END IF;
47172
47577
  SELECT o."owner_role" INTO v_owner FROM "__lattice_owners" o
47173
47578
  WHERE o."table_name" = p_table AND o."pk" = p_pk;
47174
47579
  IF v_owner IS NULL OR v_owner <> session_user THEN
@@ -47201,6 +47606,87 @@ RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
47201
47606
  SELECT lattice_row_visible('files', p_source_ref)
47202
47607
  $fn$;
47203
47608
 
47609
+ -- Is the connected member the OWNER of this row? Used by the "owner" column
47610
+ -- audience (a secret column reveals only to the row owner). SECURITY DEFINER +
47611
+ -- session_user, like the other predicates.
47612
+ CREATE OR REPLACE FUNCTION lattice_is_owner(p_table text, p_pk text)
47613
+ RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
47614
+ SELECT EXISTS (
47615
+ SELECT 1 FROM "__lattice_owners" o
47616
+ WHERE o."table_name" = p_table AND o."pk" = p_pk AND o."owner_role" = session_user
47617
+ )
47618
+ $fn$;
47619
+
47620
+ -- Owner-only: set a table's default row visibility for NEW rows. Raises unless the
47621
+ -- caller can create roles (a cloud owner / DBA), like lattice_assign_role. Rejects
47622
+ -- 'everyone' on a never-share table.
47623
+ CREATE OR REPLACE FUNCTION lattice_set_table_default_visibility(p_table text, p_visibility text)
47624
+ RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
47625
+ BEGIN
47626
+ IF NOT (SELECT rolcreaterole FROM pg_roles WHERE rolname = session_user) THEN
47627
+ RAISE EXCEPTION 'lattice: only a cloud owner may set a table''s default visibility';
47628
+ END IF;
47629
+ IF p_visibility NOT IN ('private','everyone') THEN
47630
+ RAISE EXCEPTION 'lattice: invalid default visibility %', p_visibility;
47631
+ END IF;
47632
+ IF p_visibility = 'everyone'
47633
+ AND COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = p_table), false) THEN
47634
+ RAISE EXCEPTION 'lattice: "%" is a private-only table; its rows cannot default to everyone', p_table;
47635
+ END IF;
47636
+ INSERT INTO "__lattice_table_policy" ("table_name","default_row_visibility","updated_by","updated_at")
47637
+ VALUES (p_table, p_visibility, session_user, now())
47638
+ ON CONFLICT ("table_name") DO UPDATE
47639
+ SET "default_row_visibility" = EXCLUDED."default_row_visibility",
47640
+ "updated_by" = session_user, "updated_at" = now();
47641
+ END $fn$;
47642
+
47643
+ -- Owner-only: mark a table never-shareable (Secrets/Messages-class). When true the
47644
+ -- share/grant functions raise and the insert trigger forces new rows private; the
47645
+ -- default visibility is also forced private. Turning it ON also RETROACTIVELY
47646
+ -- privatizes the table: any row currently shared ('everyone'/'custom') is reset to
47647
+ -- 'private' and every existing row/cell grant on the table is dropped \u2014 otherwise
47648
+ -- flagging a table never-share would leave already-leaked rows visible, defeating
47649
+ -- the point. Idempotent: re-running with already-private rows updates nothing.
47650
+ CREATE OR REPLACE FUNCTION lattice_set_table_never_share(p_table text, p_on boolean)
47651
+ RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
47652
+ BEGIN
47653
+ IF NOT (SELECT rolcreaterole FROM pg_roles WHERE rolname = session_user) THEN
47654
+ RAISE EXCEPTION 'lattice: only a cloud owner may change a table''s never-share flag';
47655
+ END IF;
47656
+ INSERT INTO "__lattice_table_policy" ("table_name","never_share","default_row_visibility","updated_by","updated_at")
47657
+ VALUES (p_table, p_on, CASE WHEN p_on THEN 'private' ELSE 'private' END, session_user, now())
47658
+ ON CONFLICT ("table_name") DO UPDATE
47659
+ SET "never_share" = EXCLUDED."never_share",
47660
+ "default_row_visibility" = CASE WHEN EXCLUDED."never_share"
47661
+ THEN 'private' ELSE "__lattice_table_policy"."default_row_visibility" END,
47662
+ "updated_by" = session_user, "updated_at" = now();
47663
+ IF p_on THEN
47664
+ UPDATE "__lattice_owners" SET "visibility" = 'private', "updated_at" = now()
47665
+ WHERE "table_name" = p_table AND "visibility" <> 'private';
47666
+ DELETE FROM "__lattice_row_grants" WHERE "table_name" = p_table;
47667
+ DELETE FROM "__lattice_cell_grants" WHERE "table_name" = p_table;
47668
+ END IF;
47669
+ END $fn$;
47670
+
47671
+ -- Owner-only: set (or clear) a column's audience spec in the canonical DB store.
47672
+ -- An empty/null spec removes the policy row (column becomes unmasked). The GUI/lib
47673
+ -- regenerates the table's mask view from this store after calling this.
47674
+ CREATE OR REPLACE FUNCTION lattice_set_column_audience(p_table text, p_column text, p_audience text)
47675
+ RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
47676
+ BEGIN
47677
+ IF NOT (SELECT rolcreaterole FROM pg_roles WHERE rolname = session_user) THEN
47678
+ RAISE EXCEPTION 'lattice: only a cloud owner may set a column audience';
47679
+ END IF;
47680
+ IF p_audience IS NULL OR btrim(p_audience) = '' THEN
47681
+ DELETE FROM "__lattice_column_policy" WHERE "table_name" = p_table AND "column_name" = p_column;
47682
+ ELSE
47683
+ INSERT INTO "__lattice_column_policy" ("table_name","column_name","audience","updated_by","updated_at")
47684
+ VALUES (p_table, p_column, p_audience, session_user, now())
47685
+ ON CONFLICT ("table_name","column_name") DO UPDATE
47686
+ SET "audience" = EXCLUDED."audience", "updated_by" = session_user, "updated_at" = now();
47687
+ END IF;
47688
+ END $fn$;
47689
+
47204
47690
  -- Append-only change feed. The per-table ownership trigger records one row per
47205
47691
  -- INSERT/UPDATE/DELETE; the AFTER INSERT trigger here fires pg_notify so a
47206
47692
  -- connected member's realtime broker refreshes. Members get no direct access \u2014
@@ -47232,6 +47718,62 @@ END $fn$;
47232
47718
  DROP TRIGGER IF EXISTS "lattice_notify_change_trg" ON "__lattice_changes";
47233
47719
  CREATE TRIGGER "lattice_notify_change_trg" AFTER INSERT ON "__lattice_changes"
47234
47720
  FOR EACH ROW EXECUTE FUNCTION lattice_notify_change();
47721
+
47722
+ -- #4.4 \u2014 seq-based catch-up after a realtime gap. NOTIFY is fire-and-forget, so a
47723
+ -- broker that drops its LISTEN (network blip, laptop sleep) misses every change
47724
+ -- during the gap. The broker tracks the highest seq it delivered and, on
47725
+ -- reconnect, replays what it missed via this function. Members have NO direct
47726
+ -- grant on __lattice_changes (reading it raw would leak every change on the
47727
+ -- cloud), so this SECURITY DEFINER function is the only path and it returns ONLY
47728
+ -- the rows the CALLING role can see: keyed on session_user via lattice_row_visible
47729
+ -- (same gate as live fan-out, #4.3). Deletes are excluded \u2014 the ownership record
47730
+ -- is gone post-delete so visibility can't be verified, and replaying them would
47731
+ -- leak deleted-row pks (the client reconciles deletes on its reconnect refetch).
47732
+ -- Bounded (LIMIT clamped \u2264 1000) so a long gap can't stream the whole table (Rule:
47733
+ -- bounded reads on a hot path).
47734
+ CREATE OR REPLACE FUNCTION lattice_changes_since(p_seq bigint, p_limit int)
47735
+ RETURNS TABLE(seq bigint, table_name text, pk text, op text, owner_role text, created_at timestamptz)
47736
+ LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
47737
+ SELECT c."seq", c."table_name", c."pk", c."op", c."owner_role", c."created_at"
47738
+ FROM "__lattice_changes" c
47739
+ WHERE c."seq" > p_seq
47740
+ AND c."op" = 'upsert'
47741
+ AND lattice_row_visible(c."table_name", c."pk")
47742
+ ORDER BY c."seq" ASC
47743
+ LIMIT GREATEST(0, LEAST(COALESCE(p_limit, 500), 1000));
47744
+ $fn$;
47745
+
47746
+ -- #2.1 \u2014 per-row access summary for the connecting role. The GUI attaches this as
47747
+ -- each row's _access so the sharing affordance renders, but __lattice_owners is
47748
+ -- owner-only bookkeeping (members have no grant), so a member reading it directly
47749
+ -- got "permission denied". This SECURITY DEFINER function returns visibility +
47750
+ -- whether the CALLER owns the row, ONLY for the rows the caller can actually see
47751
+ -- (lattice_row_visible, keyed on session_user) \u2014 so a member learns nothing about
47752
+ -- rows hidden from it. Member-callable; the owner gets the same view of its rows.
47753
+ CREATE OR REPLACE FUNCTION lattice_rows_access(p_table text, p_pks text[])
47754
+ RETURNS TABLE(pk text, visibility text, owned boolean)
47755
+ LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
47756
+ SELECT o."pk", o."visibility", (o."owner_role" = session_user) AS owned
47757
+ FROM "__lattice_owners" o
47758
+ WHERE o."table_name" = p_table
47759
+ AND o."pk" = ANY(p_pks)
47760
+ AND lattice_row_visible(o."table_name", o."pk");
47761
+ $fn$;
47762
+
47763
+ -- #2.1 \u2014 grantees of a CALLER-OWNED custom-shared row (who you shared YOUR row
47764
+ -- with). Only the row owner sees this (the WHERE pins owner_role = session_user),
47765
+ -- so a member can't enumerate another owner's grants. __lattice_row_grants is
47766
+ -- member-ungranted, so this SECURITY DEFINER function is the member-safe path.
47767
+ CREATE OR REPLACE FUNCTION lattice_row_grantees(p_table text, p_pks text[])
47768
+ RETURNS TABLE(pk text, grantee_role text)
47769
+ LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
47770
+ SELECT g."pk", g."grantee_role"
47771
+ FROM "__lattice_row_grants" g
47772
+ JOIN "__lattice_owners" o ON o."table_name" = g."table_name" AND o."pk" = g."pk"
47773
+ WHERE g."table_name" = p_table
47774
+ AND g."pk" = ANY(p_pks)
47775
+ AND o."owner_role" = session_user;
47776
+ $fn$;
47235
47777
  `;
47236
47778
  function tableRlsSql(table, pkCols) {
47237
47779
  const q3 = `"${table.replace(/"/g, '""')}"`;
@@ -47245,7 +47787,20 @@ CREATE OR REPLACE FUNCTION "${trg}"() RETURNS trigger LANGUAGE plpgsql SECURITY
47245
47787
  BEGIN
47246
47788
  IF TG_OP = 'INSERT' THEN
47247
47789
  INSERT INTO "__lattice_owners" ("table_name","pk","owner_role","visibility")
47248
- VALUES (${lit}, ${pkNew}, session_user, 'private')
47790
+ VALUES (${lit}, ${pkNew}, session_user,
47791
+ CASE
47792
+ -- never-share always wins: such a table's rows are private, full stop.
47793
+ WHEN COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = ${lit}), false)
47794
+ THEN 'private'
47795
+ -- per-INSERT override: a caller forcing visibility for THIS write (e.g.
47796
+ -- chat "private mode") sets the transaction-local lattice.force_row_visibility
47797
+ -- GUC, so the row is stamped atomically at insert \u2014 never momentarily at
47798
+ -- the table default, and the change-feed NOTIFY (deferred to COMMIT) only
47799
+ -- fires once the row already carries this visibility.
47800
+ WHEN NULLIF(current_setting('lattice.force_row_visibility', true), '') IN ('private','everyone')
47801
+ THEN current_setting('lattice.force_row_visibility', true)
47802
+ ELSE COALESCE((SELECT "default_row_visibility" FROM "__lattice_table_policy" WHERE "table_name" = ${lit}), 'private')
47803
+ END)
47249
47804
  ON CONFLICT ("table_name","pk") DO NOTHING;
47250
47805
  INSERT INTO "__lattice_changes" ("table_name","pk","op","owner_role")
47251
47806
  VALUES (${lit}, ${pkNew}, 'upsert', session_user);
@@ -47285,20 +47840,15 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
47285
47840
  }
47286
47841
  async function installCloudRls(db) {
47287
47842
  if (!isPg(db)) return;
47288
- const migration = {
47289
- // v3 added the audience helpers; v4 the role model; v5 the per-card override
47290
- // model (__lattice_cell_grants + lattice_cell_visible / lattice_grant_cell).
47291
- // The bootstrap is fully idempotent (CREATE OR REPLACE / IF NOT EXISTS).
47292
- version: "internal:cloud-rls:bootstrap:v5",
47293
- sql: CLOUD_RLS_BOOTSTRAP_SQL
47294
- };
47295
- await db.migrate([migration]);
47843
+ const schema = await cloudSchema(db);
47844
+ const sql = pinDefinerSearchPath(CLOUD_RLS_BOOTSTRAP_SQL, schema) + revokeSchemaCreateSql(schema);
47845
+ await runCloudBootstrapSql(db, sql);
47296
47846
  }
47297
47847
  async function enableChangelogRls(db) {
47298
47848
  if (!isPg(db)) return;
47299
- const migration = {
47300
- version: "internal:cloud-rls:changelog:v1",
47301
- sql: `
47849
+ await runCloudBootstrapSql(
47850
+ db,
47851
+ `
47302
47852
  ALTER TABLE "__lattice_changelog" ENABLE ROW LEVEL SECURITY;
47303
47853
  ALTER TABLE "__lattice_changelog" FORCE ROW LEVEL SECURITY;
47304
47854
  GRANT SELECT, INSERT ON "__lattice_changelog" TO ${MEMBER_GROUP};
@@ -47308,24 +47858,25 @@ CREATE POLICY "lattice_changelog_sel" ON "__lattice_changelog" FOR SELECT USING
47308
47858
  CASE
47309
47859
  WHEN "change_kind" = 'derived' THEN
47310
47860
  "source_ref" IS NOT NULL
47861
+ AND jsonb_array_length("source_ref"::jsonb) > 0
47311
47862
  AND NOT EXISTS (
47312
47863
  SELECT 1 FROM jsonb_array_elements_text("source_ref"::jsonb) AS src(sid)
47313
47864
  WHERE NOT lattice_source_visible(src.sid)
47314
47865
  )
47315
- ELSE lattice_row_visible("table_name", "row_id")
47866
+ ELSE lattice_is_owner("table_name", "row_id")
47316
47867
  END
47317
47868
  );
47318
47869
  DROP POLICY IF EXISTS "lattice_changelog_ins" ON "__lattice_changelog";
47319
47870
  CREATE POLICY "lattice_changelog_ins" ON "__lattice_changelog" FOR INSERT WITH CHECK (true);
47320
47871
  `
47321
- };
47322
- await db.migrate([migration]);
47872
+ );
47323
47873
  }
47324
47874
  async function enableRlsForTable(db, table, pkCols) {
47325
47875
  if (!isPg(db)) return;
47876
+ const schema = await cloudSchema(db);
47326
47877
  const migration = {
47327
- version: `internal:cloud-rls:table:${table}:v2`,
47328
- sql: tableRlsSql(table, pkCols)
47878
+ version: `internal:cloud-rls:table:${table}:v3`,
47879
+ sql: pinDefinerSearchPath(tableRlsSql(table, pkCols), schema)
47329
47880
  };
47330
47881
  await db.migrate([migration]);
47331
47882
  }
@@ -47372,7 +47923,12 @@ async function provisionMemberRole(db, role, password) {
47372
47923
  IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${role}') THEN
47373
47924
  CREATE ROLE "${role}" LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE PASSWORD '${password}';
47374
47925
  ELSE
47375
- ALTER ROLE "${role}" WITH LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE PASSWORD '${password}';
47926
+ -- Re-invite of an EXISTING role: set ONLY what changed (login + password).
47927
+ -- Restating NOSUPERUSER/superuser-class attrs trips Supabase supautils
47928
+ -- ("only superuser may alter the SUPERUSER attribute", 42501) since the
47929
+ -- owner 'postgres' isn't a true superuser. The role was already created
47930
+ -- NOSUPERUSER NOCREATEDB NOCREATEROLE, so there is nothing to restate.
47931
+ ALTER ROLE "${role}" WITH LOGIN PASSWORD '${password}';
47376
47932
  END IF;
47377
47933
  END $LATTICE$`
47378
47934
  );
@@ -47411,9 +47967,28 @@ async function revokeCell(db, table, pk, column, grantee) {
47411
47967
  async function revokeMemberRole(db, role) {
47412
47968
  assertPg(db);
47413
47969
  if (!ROLE_RE.test(role)) throw new Error(`lattice: invalid member role name "${role}"`);
47414
- await runAsyncOrSync(db.adapter, `DROP OWNED BY "${role}"`).catch(() => void 0);
47970
+ const exists = await getAsyncOrSync(
47971
+ db.adapter,
47972
+ `SELECT 1 AS x FROM pg_roles WHERE rolname = ?`,
47973
+ [role]
47974
+ );
47975
+ if (!exists) return;
47976
+ for (const stmt of [`REASSIGN OWNED BY "${role}" TO CURRENT_USER`, `DROP OWNED BY "${role}"`]) {
47977
+ try {
47978
+ await runAsyncOrSync(db.adapter, stmt);
47979
+ } catch (e6) {
47980
+ if (!isInsufficientPrivilege(e6)) throw e6;
47981
+ console.warn(
47982
+ `[cloud] "${stmt.split(" ").slice(0, 2).join(" ")} \u2026" skipped (insufficient privilege; a scoped member owns no objects): ${e6.message}`
47983
+ );
47984
+ }
47985
+ }
47415
47986
  await runAsyncOrSync(db.adapter, `DROP ROLE IF EXISTS "${role}"`);
47416
47987
  }
47988
+ function isInsufficientPrivilege(e6) {
47989
+ const err = e6 ?? {};
47990
+ return err.code === "42501" || /permission denied/i.test(err.message ?? "");
47991
+ }
47417
47992
 
47418
47993
  // src/cloud/discover.ts
47419
47994
  async function discoverCloudTables(db) {
@@ -47455,12 +48030,17 @@ function isRowAudience(audience) {
47455
48030
  const a6 = (audience ?? "").trim();
47456
48031
  return a6 === "" || a6 === "everyone" || a6 === "row-audience";
47457
48032
  }
47458
- function audiencePredicate(audience) {
48033
+ function audiencePredicate(audience, ctx) {
47459
48034
  if (isRowAudience(audience)) return "true";
47460
48035
  const clauses = audience.split("+").map((c6) => c6.trim()).filter(Boolean);
47461
48036
  const parts = [];
47462
48037
  for (const clause of clauses) {
47463
48038
  if (clause === "everyone" || clause === "row-audience") return "true";
48039
+ if (clause === "owner") {
48040
+ if (!ctx) throw new Error('lattice: the "owner" audience needs a row context');
48041
+ parts.push(`lattice_is_owner(${ctx.tableLit}, ${ctx.pkExpr})`);
48042
+ continue;
48043
+ }
47464
48044
  const idx = clause.indexOf(":");
47465
48045
  const kind = idx === -1 ? clause : clause.slice(0, idx);
47466
48046
  const arg = idx === -1 ? "" : clause.slice(idx + 1).trim();
@@ -47495,7 +48075,7 @@ function audienceViewSql(table, columns, pkCols, columnAudience) {
47495
48075
  const selectCols = columns.map((col) => {
47496
48076
  const aud = columnAudience[col] ?? "";
47497
48077
  if (isRowAudience(aud)) return quoteIdent(col);
47498
- const pred = audiencePredicate(aud);
48078
+ const pred = audiencePredicate(aud, { tableLit: lit, pkExpr });
47499
48079
  if (pred === "true") return quoteIdent(col);
47500
48080
  const colLit = `'${col.replace(/'/g, "''")}'`;
47501
48081
  const full = `(${pred}) OR lattice_cell_visible(${lit}, ${pkExpr}, ${colLit})`;
@@ -47530,6 +48110,92 @@ async function enableAudienceView(db, table, columns, pkCols, columnAudience) {
47530
48110
  };
47531
48111
  await db.migrate([migration]);
47532
48112
  }
48113
+ async function loadColumnPolicy(db, table) {
48114
+ if (db.getDialect() !== "postgres") return {};
48115
+ const rows = await allAsyncOrSync(
48116
+ db.adapter,
48117
+ `SELECT "column_name", "audience" FROM "__lattice_column_policy" WHERE "table_name" = ?`,
48118
+ [table]
48119
+ );
48120
+ const out = {};
48121
+ for (const r6 of rows) out[r6.column_name] = r6.audience;
48122
+ return out;
48123
+ }
48124
+ async function seedColumnPolicyFromYaml(db, table, yamlAudience) {
48125
+ if (db.getDialect() !== "postgres") return;
48126
+ const marker = `internal:cloud-column-seed:${table}:v1`;
48127
+ const already = await getAsyncOrSync(
48128
+ db.adapter,
48129
+ `SELECT 1 AS one FROM "__lattice_migrations" WHERE "version" = ?`,
48130
+ [marker]
48131
+ );
48132
+ if (already) return;
48133
+ for (const [col, aud] of Object.entries(yamlAudience)) {
48134
+ if (isRowAudience(aud)) continue;
48135
+ await runAsyncOrSync(
48136
+ db.adapter,
48137
+ `INSERT INTO "__lattice_column_policy" ("table_name","column_name","audience")
48138
+ VALUES (?, ?, ?) ON CONFLICT ("table_name","column_name") DO NOTHING`,
48139
+ [table, col, aud]
48140
+ );
48141
+ }
48142
+ await runAsyncOrSync(
48143
+ db.adapter,
48144
+ `INSERT INTO "__lattice_migrations" ("version","applied_at") VALUES (?, ?)
48145
+ ON CONFLICT ("version") DO NOTHING`,
48146
+ [marker, (/* @__PURE__ */ new Date()).toISOString()]
48147
+ );
48148
+ }
48149
+ async function regenerateAudienceViewFromDb(db, table, columns, pkCols) {
48150
+ if (db.getDialect() !== "postgres") return;
48151
+ if (pkCols.length === 0) return;
48152
+ const spec = await loadColumnPolicy(db, table);
48153
+ const view = quoteIdent(`${table}_v`);
48154
+ const base = quoteIdent(table);
48155
+ if (!tableNeedsAudienceView(spec)) {
48156
+ await runAsyncOrSync(
48157
+ db.adapter,
48158
+ `DROP VIEW IF EXISTS ${view};
48159
+ GRANT SELECT ON ${base} TO ${MEMBER_GROUP};`
48160
+ );
48161
+ return;
48162
+ }
48163
+ await runAsyncOrSync(db.adapter, audienceViewSql(table, columns, pkCols, spec));
48164
+ }
48165
+ async function setColumnAudience(db, table, column, audience, columns, pkCols) {
48166
+ if (db.getDialect() !== "postgres") return;
48167
+ await runAsyncOrSync(db.adapter, `SELECT lattice_set_column_audience(?, ?, ?)`, [
48168
+ table,
48169
+ column,
48170
+ audience
48171
+ ]);
48172
+ await regenerateAudienceViewFromDb(db, table, columns, pkCols);
48173
+ }
48174
+
48175
+ // src/cloud/table-policy.ts
48176
+ async function getTablePolicy(db, table) {
48177
+ if (db.getDialect() !== "postgres") return { defaultRowVisibility: "private", neverShare: false };
48178
+ const row = await getAsyncOrSync(
48179
+ db.adapter,
48180
+ `SELECT "default_row_visibility", "never_share" FROM "__lattice_table_policy" WHERE "table_name" = ?`,
48181
+ [table]
48182
+ );
48183
+ return {
48184
+ defaultRowVisibility: row?.default_row_visibility === "everyone" ? "everyone" : "private",
48185
+ neverShare: row?.never_share === true
48186
+ };
48187
+ }
48188
+ async function setTableDefaultVisibility(db, table, visibility) {
48189
+ if (db.getDialect() !== "postgres") return;
48190
+ await runAsyncOrSync(db.adapter, `SELECT lattice_set_table_default_visibility(?, ?)`, [
48191
+ table,
48192
+ visibility
48193
+ ]);
48194
+ }
48195
+ async function setTableNeverShare(db, table, on) {
48196
+ if (db.getDialect() !== "postgres") return;
48197
+ await runAsyncOrSync(db.adapter, `SELECT lattice_set_table_never_share(?, ?)`, [table, on]);
48198
+ }
47533
48199
 
47534
48200
  // src/cloud/fold-cache.ts
47535
48201
  function viewerSignature(viewer) {
@@ -47566,6 +48232,7 @@ var FoldCache = class {
47566
48232
  };
47567
48233
 
47568
48234
  // src/cloud/settings.ts
48235
+ import { createHash as createHash8, randomBytes as randomBytes6 } from "crypto";
47569
48236
  var CLOUD_SETTING_SYSTEM_PROMPT = "chat_system_prompt";
47570
48237
  var CLOUD_SETTINGS_BOOTSTRAP_SQL = `
47571
48238
  -- Owner-controlled, cloud-wide key/value settings. No grant to the member group,
@@ -47604,11 +48271,8 @@ END $fn$;
47604
48271
  `;
47605
48272
  async function installCloudSettings(db) {
47606
48273
  if (db.getDialect() !== "postgres") return;
47607
- const migration = {
47608
- version: "internal:cloud-settings:v1",
47609
- sql: CLOUD_SETTINGS_BOOTSTRAP_SQL
47610
- };
47611
- await db.migrate([migration]);
48274
+ const schema = await cloudSchema(db);
48275
+ await runCloudBootstrapSql(db, pinDefinerSearchPath(CLOUD_SETTINGS_BOOTSTRAP_SQL, schema));
47612
48276
  }
47613
48277
  async function getCloudSetting(db, key) {
47614
48278
  if (db.getDialect() !== "postgres") return null;
@@ -47627,23 +48291,39 @@ async function setCloudSetting(db, key, value) {
47627
48291
  }
47628
48292
 
47629
48293
  // src/cloud/setup.ts
48294
+ async function secureNewCloudTable(db, table, pk) {
48295
+ if (db.getDialect() !== "postgres") return;
48296
+ if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) return;
48297
+ if (pk.length === 0) return;
48298
+ await backfillOwnership(db, table, pk);
48299
+ await enableRlsForTable(db, table, pk);
48300
+ const cols = db.getRegisteredColumns(table);
48301
+ if (cols) {
48302
+ await seedColumnPolicyFromYaml(db, table, db.getColumnAudience(table));
48303
+ await regenerateAudienceViewFromDb(db, table, Object.keys(cols), pk);
48304
+ }
48305
+ }
47630
48306
  async function secureCloud(db) {
47631
48307
  if (db.getDialect() !== "postgres") return;
47632
48308
  await installCloudRls(db);
47633
48309
  await installCloudSettings(db);
47634
48310
  await db.ensureObservationSubstrate();
47635
48311
  await enableChangelogRls(db);
47636
- for (const table of db.getRegisteredTableNames()) {
47637
- if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) continue;
47638
- const pk = db.getPrimaryKey(table);
47639
- if (pk.length === 0) continue;
47640
- await backfillOwnership(db, table, pk);
47641
- await enableRlsForTable(db, table, pk);
47642
- const cols = db.getRegisteredColumns(table);
47643
- if (cols) {
47644
- await enableAudienceView(db, table, Object.keys(cols), pk, db.getColumnAudience(table));
47645
- }
48312
+ const registered = db.getRegisteredTableNames();
48313
+ for (const table of registered) {
48314
+ await secureNewCloudTable(db, table, db.getPrimaryKey(table));
48315
+ }
48316
+ if (registered.includes("secrets")) {
48317
+ await runAsyncOrSync(db.adapter, `SELECT lattice_set_table_never_share('secrets', true)`);
47646
48318
  }
48319
+ await runAsyncOrSync(
48320
+ db.adapter,
48321
+ `DO $LATTICE$ BEGIN
48322
+ IF to_regclass('__lattice_user_identity') IS NOT NULL THEN
48323
+ EXECUTE 'GRANT SELECT, INSERT, UPDATE ON "__lattice_user_identity" TO ${MEMBER_GROUP}';
48324
+ END IF;
48325
+ END $LATTICE$`
48326
+ );
47647
48327
  }
47648
48328
 
47649
48329
  // src/ai/llm-client.ts
@@ -48237,6 +48917,7 @@ export {
48237
48917
  NATIVE_ENTITY_NAMES,
48238
48918
  NATIVE_REGISTRY_TABLE,
48239
48919
  PostgresAdapter,
48920
+ ProgressThrottle,
48240
48921
  READ_ONLY_HEADER,
48241
48922
  ROOT_DIRNAME,
48242
48923
  ReferenceUnavailableError,
@@ -48300,6 +48981,7 @@ export {
48300
48981
  getCloudSetting,
48301
48982
  getDbCredential,
48302
48983
  getOrCreateMasterKey,
48984
+ getTablePolicy,
48303
48985
  getWorkspace,
48304
48986
  grantCell,
48305
48987
  hasFtsIndex,
@@ -48317,6 +48999,7 @@ export {
48317
48999
  listNativeBindings,
48318
49000
  listTokens,
48319
49001
  listWorkspaces,
49002
+ loadColumnPolicy,
48320
49003
  manifestPath,
48321
49004
  markdownTable,
48322
49005
  memberRoleName,
@@ -48344,6 +49027,7 @@ export {
48344
49027
  readToken,
48345
49028
  referenceLocalFile,
48346
49029
  referenceUrl,
49030
+ regenerateAudienceViewFromDb,
48347
49031
  registerNativeEntities,
48348
49032
  registryPath,
48349
49033
  resolveActiveS3Config,
@@ -48358,9 +49042,13 @@ export {
48358
49042
  saveDbCredentialForTeam,
48359
49043
  sealUnderSource,
48360
49044
  secureCloud,
49045
+ seedColumnPolicyFromYaml,
48361
49046
  setActiveWorkspace,
48362
49047
  setCloudSetting,
49048
+ setColumnAudience,
48363
49049
  setRowVisibility,
49050
+ setTableDefaultVisibility,
49051
+ setTableNeverShare,
48364
49052
  shredSource,
48365
49053
  slugify,
48366
49054
  summarizeText,