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.cjs CHANGED
@@ -39405,6 +39405,7 @@ __export(index_exports, {
39405
39405
  NATIVE_ENTITY_NAMES: () => NATIVE_ENTITY_NAMES,
39406
39406
  NATIVE_REGISTRY_TABLE: () => NATIVE_REGISTRY_TABLE,
39407
39407
  PostgresAdapter: () => PostgresAdapter,
39408
+ ProgressThrottle: () => ProgressThrottle,
39408
39409
  READ_ONLY_HEADER: () => READ_ONLY_HEADER,
39409
39410
  ROOT_DIRNAME: () => ROOT_DIRNAME,
39410
39411
  ReferenceUnavailableError: () => ReferenceUnavailableError,
@@ -39468,6 +39469,7 @@ __export(index_exports, {
39468
39469
  getCloudSetting: () => getCloudSetting,
39469
39470
  getDbCredential: () => getDbCredential,
39470
39471
  getOrCreateMasterKey: () => getOrCreateMasterKey,
39472
+ getTablePolicy: () => getTablePolicy,
39471
39473
  getWorkspace: () => getWorkspace,
39472
39474
  grantCell: () => grantCell,
39473
39475
  hasFtsIndex: () => hasFtsIndex,
@@ -39485,6 +39487,7 @@ __export(index_exports, {
39485
39487
  listNativeBindings: () => listNativeBindings,
39486
39488
  listTokens: () => listTokens,
39487
39489
  listWorkspaces: () => listWorkspaces,
39490
+ loadColumnPolicy: () => loadColumnPolicy,
39488
39491
  manifestPath: () => manifestPath,
39489
39492
  markdownTable: () => markdownTable,
39490
39493
  memberRoleName: () => memberRoleName,
@@ -39512,6 +39515,7 @@ __export(index_exports, {
39512
39515
  readToken: () => readToken,
39513
39516
  referenceLocalFile: () => referenceLocalFile,
39514
39517
  referenceUrl: () => referenceUrl,
39518
+ regenerateAudienceViewFromDb: () => regenerateAudienceViewFromDb,
39515
39519
  registerNativeEntities: () => registerNativeEntities,
39516
39520
  registryPath: () => registryPath,
39517
39521
  resolveActiveS3Config: () => resolveActiveS3Config,
@@ -39526,9 +39530,13 @@ __export(index_exports, {
39526
39530
  saveDbCredentialForTeam: () => saveDbCredentialForTeam,
39527
39531
  sealUnderSource: () => sealUnderSource,
39528
39532
  secureCloud: () => secureCloud,
39533
+ seedColumnPolicyFromYaml: () => seedColumnPolicyFromYaml,
39529
39534
  setActiveWorkspace: () => setActiveWorkspace,
39530
39535
  setCloudSetting: () => setCloudSetting,
39536
+ setColumnAudience: () => setColumnAudience,
39531
39537
  setRowVisibility: () => setRowVisibility,
39538
+ setTableDefaultVisibility: () => setTableDefaultVisibility,
39539
+ setTableNeverShare: () => setTableNeverShare,
39532
39540
  shredSource: () => shredSource,
39533
39541
  slugify: () => slugify,
39534
39542
  summarizeText: () => summarizeText,
@@ -41616,7 +41624,68 @@ function cleanupEntityContexts(outputDir, entityContexts, currentSlugsByTable, m
41616
41624
  return result;
41617
41625
  }
41618
41626
 
41627
+ // src/render/progress.ts
41628
+ var THROTTLE_WINDOW_MS = 200;
41629
+ var ProgressThrottle = class {
41630
+ cb;
41631
+ windowMs;
41632
+ /**
41633
+ * Last passthrough time, keyed per table (`event.table`, or `''` for the
41634
+ * table-less `done`/`error` lifecycle events). Per-table — not a single shared
41635
+ * clock — so when tables render CONCURRENTLY each one keeps its own ~5/sec
41636
+ * budget: a fast table can't consume the window and starve a slow table's
41637
+ * progress. `force` (table-start) resets only that table's budget.
41638
+ */
41639
+ lastEmit = /* @__PURE__ */ new Map();
41640
+ constructor(cb, windowMs = THROTTLE_WINDOW_MS) {
41641
+ this.cb = cb;
41642
+ this.windowMs = windowMs;
41643
+ }
41644
+ /**
41645
+ * Emit a `table-progress` event, but only if the window since this table's
41646
+ * last passthrough has elapsed. Dropped events are simply not delivered — the
41647
+ * next one that survives carries the latest running count.
41648
+ */
41649
+ tick(event) {
41650
+ if (!this.cb) return;
41651
+ const key = event.table ?? "";
41652
+ const now = Date.now();
41653
+ if (now - (this.lastEmit.get(key) ?? 0) < this.windowMs) return;
41654
+ this.lastEmit.set(key, now);
41655
+ this.cb(event);
41656
+ }
41657
+ /**
41658
+ * Emit a lifecycle event immediately and reset this table's throttle window.
41659
+ * Use for `table-start`, `table-done`, `done`, and `error` — none of which
41660
+ * should ever be dropped. Resetting on `table-start` gives each table a clean
41661
+ * budget.
41662
+ */
41663
+ force(event) {
41664
+ if (!this.cb) return;
41665
+ this.lastEmit.set(event.table ?? "", Date.now());
41666
+ this.cb(event);
41667
+ }
41668
+ };
41669
+
41670
+ // src/concurrency.ts
41671
+ async function mapWithConcurrency(items, limit, fn) {
41672
+ const results = new Array(items.length);
41673
+ let next = 0;
41674
+ const workerCount = Math.max(1, Math.min(limit, items.length));
41675
+ const workers = Array.from({ length: workerCount }, async () => {
41676
+ for (; ; ) {
41677
+ const i6 = next++;
41678
+ if (i6 >= items.length) break;
41679
+ results[i6] = await fn(items[i6], i6);
41680
+ }
41681
+ });
41682
+ await Promise.all(workers);
41683
+ return results;
41684
+ }
41685
+
41619
41686
  // src/render/engine.ts
41687
+ var YIELD_EVERY_ENTITIES = 200;
41688
+ var RENDER_TABLE_CONCURRENCY = 4;
41620
41689
  var NOOP_RENDER = () => "";
41621
41690
  var RenderEngine = class {
41622
41691
  _schema;
@@ -41630,11 +41699,14 @@ var RenderEngine = class {
41630
41699
  this._getTaskContext = getTaskContext ?? (() => "");
41631
41700
  this._skipEmpty = options?.skipEmpty ?? false;
41632
41701
  }
41633
- async render(outputDir) {
41702
+ async render(outputDir, opts = {}) {
41634
41703
  const start = Date.now();
41635
41704
  const filesWritten = [];
41636
41705
  const counters = { skipped: 0 };
41706
+ const signal = opts.signal;
41707
+ const throttle = new ProgressThrottle(opts.onProgress);
41637
41708
  for (const [name, def] of this._schema.getTables()) {
41709
+ if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
41638
41710
  if (this._skipEmpty && def.render === NOOP_RENDER) continue;
41639
41711
  let rows = await this._schema.queryTable(this._adapter, name);
41640
41712
  if (def.relevanceFilter) {
@@ -41676,8 +41748,18 @@ var RenderEngine = class {
41676
41748
  } else {
41677
41749
  counters.skipped++;
41678
41750
  }
41751
+ throttle.force({
41752
+ kind: "table-done",
41753
+ table: name,
41754
+ entitiesRendered: rows.length,
41755
+ entitiesTotal: rows.length,
41756
+ tableIndex: 0,
41757
+ tableCount: 0,
41758
+ pct: 100
41759
+ });
41679
41760
  }
41680
- for (const [, def] of this._schema.getMultis()) {
41761
+ for (const [name, def] of this._schema.getMultis()) {
41762
+ if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
41681
41763
  const keys = await def.keys();
41682
41764
  const tables = {};
41683
41765
  if (def.tables) {
@@ -41694,12 +41776,26 @@ var RenderEngine = class {
41694
41776
  counters.skipped++;
41695
41777
  }
41696
41778
  }
41779
+ throttle.force({
41780
+ kind: "table-done",
41781
+ table: name,
41782
+ entitiesRendered: keys.length,
41783
+ entitiesTotal: keys.length,
41784
+ tableIndex: 0,
41785
+ tableCount: 0,
41786
+ pct: 100
41787
+ });
41697
41788
  }
41698
41789
  const entityContextManifest = await this._renderEntityContexts(
41699
41790
  outputDir,
41700
41791
  filesWritten,
41701
- counters
41792
+ counters,
41793
+ throttle,
41794
+ signal
41702
41795
  );
41796
+ if (entityContextManifest === null) {
41797
+ return this._abortedResult(filesWritten, counters, start);
41798
+ }
41703
41799
  if (this._schema.getEntityContexts().size > 0) {
41704
41800
  writeManifest(outputDir, {
41705
41801
  version: 2,
@@ -41707,6 +41803,29 @@ var RenderEngine = class {
41707
41803
  entityContexts: entityContextManifest
41708
41804
  });
41709
41805
  }
41806
+ const result = {
41807
+ filesWritten,
41808
+ filesSkipped: counters.skipped,
41809
+ durationMs: Date.now() - start
41810
+ };
41811
+ throttle.force({
41812
+ kind: "done",
41813
+ table: null,
41814
+ entitiesRendered: 0,
41815
+ entitiesTotal: 0,
41816
+ tableIndex: 0,
41817
+ tableCount: 0,
41818
+ pct: 100,
41819
+ durationMs: result.durationMs
41820
+ });
41821
+ return result;
41822
+ }
41823
+ /**
41824
+ * Build the partial RenderResult to return when a render is aborted. No
41825
+ * `done` event is emitted — the caller treats abort as "discard the partial
41826
+ * tree", not as a successful completion.
41827
+ */
41828
+ _abortedResult(filesWritten, counters, start) {
41710
41829
  return {
41711
41830
  filesWritten,
41712
41831
  filesSkipped: counters.skipped,
@@ -41742,127 +41861,181 @@ var RenderEngine = class {
41742
41861
  /**
41743
41862
  * Render all entity context definitions.
41744
41863
  * Mutates `filesWritten` and `counters` in place.
41745
- * Returns manifest data for the entity contexts rendered this cycle.
41864
+ * Returns manifest data for the entity contexts rendered this cycle, or
41865
+ * `null` if the render was aborted mid-flight (the caller discards the
41866
+ * partial tree). Progress is reported through `throttle`; abort is observed
41867
+ * via `signal`.
41746
41868
  */
41747
- async _renderEntityContexts(outputDir, filesWritten, counters) {
41748
- const manifestData = {};
41869
+ async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal) {
41749
41870
  const protectedTables = /* @__PURE__ */ new Set();
41750
41871
  for (const [t8, d6] of this._schema.getEntityContexts()) {
41751
41872
  if (d6.protected) protectedTables.add(t8);
41752
41873
  }
41753
- for (const [table, def] of this._schema.getEntityContexts()) {
41754
- const entityPk = this._schema.getPrimaryKey(table)[0] ?? "id";
41755
- const allRows = await this._schema.queryTable(this._adapter, table);
41756
- const directoryRoot = def.directoryRoot ?? table;
41757
- const manifestEntry = {
41758
- directoryRoot,
41759
- ...def.index ? { indexFile: def.index.outputFile } : {},
41760
- declaredFiles: Object.keys(def.files),
41761
- protectedFiles: def.protectedFiles ?? [],
41762
- entities: {}
41763
- };
41764
- if (def.index) {
41765
- const indexPath = (0, import_node_path5.join)(outputDir, def.index.outputFile);
41766
- if (atomicWrite(indexPath, def.index.render(allRows))) {
41767
- filesWritten.push(indexPath);
41768
- } else {
41769
- counters.skipped++;
41770
- }
41771
- }
41772
- for (const entityRow of allRows) {
41773
- const rawSlug = def.slug(entityRow);
41774
- const slug = rawSlug.replace(/[\u00A0\u2000-\u200B\u202F\u205F\u3000]/g, " ").replace(/[\u0000-\u001F\u007F]/g, "");
41775
- if (/[^a-zA-Z0-9.\-_ @(),#&'+:;!~[\]]/.test(slug)) {
41776
- throw new Error(`Invalid slug "${slug}": contains characters outside the allowed set`);
41777
- }
41778
- const entityDir = def.directory ? (0, import_node_path5.join)(outputDir, def.directory(entityRow)) : (0, import_node_path5.join)(outputDir, directoryRoot, slug);
41779
- const resolvedDir = (0, import_node_path5.resolve)(entityDir);
41780
- const resolvedBase = (0, import_node_path5.resolve)(outputDir);
41781
- if (!resolvedDir.startsWith(resolvedBase + import_node_path5.sep) && resolvedDir !== resolvedBase) {
41782
- throw new Error(`Path traversal detected: slug "${slug}" escapes output directory`);
41874
+ const entityTables = [...this._schema.getEntityContexts()];
41875
+ const tableCount = entityTables.length;
41876
+ if (signal?.aborted) return null;
41877
+ const renderedEntries = await mapWithConcurrency(
41878
+ entityTables,
41879
+ RENDER_TABLE_CONCURRENCY,
41880
+ async ([table, def], tableIndex) => {
41881
+ if (signal?.aborted) return null;
41882
+ const entityPk = this._schema.getPrimaryKey(table)[0] ?? "id";
41883
+ const allRows = await this._schema.queryTable(this._adapter, table);
41884
+ const directoryRoot = def.directoryRoot ?? table;
41885
+ const entitiesTotal = allRows.length;
41886
+ throttle.force({
41887
+ kind: "table-start",
41888
+ table,
41889
+ entitiesRendered: 0,
41890
+ entitiesTotal,
41891
+ tableIndex,
41892
+ tableCount,
41893
+ pct: 0
41894
+ });
41895
+ const manifestEntry = {
41896
+ directoryRoot,
41897
+ ...def.index ? { indexFile: def.index.outputFile } : {},
41898
+ declaredFiles: Object.keys(def.files),
41899
+ protectedFiles: def.protectedFiles ?? [],
41900
+ entities: {}
41901
+ };
41902
+ if (def.index) {
41903
+ const indexPath = (0, import_node_path5.join)(outputDir, def.index.outputFile);
41904
+ if (atomicWrite(indexPath, def.index.render(allRows))) {
41905
+ filesWritten.push(indexPath);
41906
+ } else {
41907
+ counters.skipped++;
41908
+ }
41783
41909
  }
41784
- (0, import_node_fs4.mkdirSync)(entityDir, { recursive: true });
41785
- if (def.attachFileColumn) {
41786
- const filePath = entityRow[def.attachFileColumn];
41787
- if (filePath && typeof filePath === "string" && filePath.length > 0) {
41788
- if (def.attachFileMode === "reference") {
41789
- const refPath = (0, import_node_path5.join)(entityDir, `${(0, import_node_path5.basename)(filePath)}.ref.md`);
41790
- try {
41791
- atomicWrite(refPath, `# Reference
41910
+ for (let i6 = 0; i6 < allRows.length; i6++) {
41911
+ const entityRow = allRows[i6];
41912
+ if (signal?.aborted) return null;
41913
+ if (i6 > 0 && i6 % YIELD_EVERY_ENTITIES === 0) {
41914
+ await new Promise((r6) => setImmediate(r6));
41915
+ }
41916
+ const rawSlug = def.slug(entityRow);
41917
+ const slug = rawSlug.replace(/[\u00A0\u2000-\u200B\u202F\u205F\u3000]/g, " ").replace(/[\u0000-\u001F\u007F]/g, "");
41918
+ if (/[^a-zA-Z0-9.\-_ @(),#&'+:;!~[\]]/.test(slug)) {
41919
+ throw new Error(`Invalid slug "${slug}": contains characters outside the allowed set`);
41920
+ }
41921
+ const entityDir = def.directory ? (0, import_node_path5.join)(outputDir, def.directory(entityRow)) : (0, import_node_path5.join)(outputDir, directoryRoot, slug);
41922
+ const resolvedDir = (0, import_node_path5.resolve)(entityDir);
41923
+ const resolvedBase = (0, import_node_path5.resolve)(outputDir);
41924
+ if (!resolvedDir.startsWith(resolvedBase + import_node_path5.sep) && resolvedDir !== resolvedBase) {
41925
+ throw new Error(`Path traversal detected: slug "${slug}" escapes output directory`);
41926
+ }
41927
+ (0, import_node_fs4.mkdirSync)(entityDir, { recursive: true });
41928
+ if (def.attachFileColumn) {
41929
+ const filePath = entityRow[def.attachFileColumn];
41930
+ if (filePath && typeof filePath === "string" && filePath.length > 0) {
41931
+ if (def.attachFileMode === "reference") {
41932
+ const refPath = (0, import_node_path5.join)(entityDir, `${(0, import_node_path5.basename)(filePath)}.ref.md`);
41933
+ try {
41934
+ atomicWrite(refPath, `# Reference
41792
41935
 
41793
41936
  - **location:** ${filePath}
41794
41937
  `);
41795
- filesWritten.push(refPath);
41796
- } catch {
41797
- }
41798
- } else {
41799
- const absPath = (0, import_node_path5.isAbsolute)(filePath) ? filePath : (0, import_node_path5.resolve)(outputDir, filePath);
41800
- if ((0, import_node_fs4.existsSync)(absPath)) {
41801
- const destPath = (0, import_node_path5.join)(entityDir, (0, import_node_path5.basename)(absPath));
41802
- if (!(0, import_node_fs4.existsSync)(destPath)) {
41803
- try {
41804
- (0, import_node_fs4.copyFileSync)(absPath, destPath);
41805
- filesWritten.push(destPath);
41806
- } catch {
41938
+ filesWritten.push(refPath);
41939
+ } catch {
41940
+ }
41941
+ } else {
41942
+ const absPath = (0, import_node_path5.isAbsolute)(filePath) ? filePath : (0, import_node_path5.resolve)(outputDir, filePath);
41943
+ if ((0, import_node_fs4.existsSync)(absPath)) {
41944
+ const destPath = (0, import_node_path5.join)(entityDir, (0, import_node_path5.basename)(absPath));
41945
+ if (!(0, import_node_fs4.existsSync)(destPath)) {
41946
+ try {
41947
+ (0, import_node_fs4.copyFileSync)(absPath, destPath);
41948
+ filesWritten.push(destPath);
41949
+ } catch {
41950
+ }
41807
41951
  }
41808
41952
  }
41809
41953
  }
41810
41954
  }
41811
41955
  }
41812
- }
41813
- const renderedFiles = /* @__PURE__ */ new Map();
41814
- const entityFileHashes = {};
41815
- const protection = protectedTables.size > 0 ? { protectedTables, currentTable: table } : void 0;
41816
- for (const [filename, spec] of Object.entries(def.files)) {
41817
- const mergeDefaults = def.sourceDefaults && spec.source.type !== "self" && spec.source.type !== "custom" && spec.source.type !== "enriched";
41818
- const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
41819
- const rows = await resolveEntitySource(
41820
- source,
41821
- entityRow,
41822
- entityPk,
41823
- this._adapter,
41824
- protection
41825
- );
41826
- if (spec.omitIfEmpty && rows.length === 0) continue;
41827
- const renderFn = compileEntityRender(spec.render);
41828
- const content = truncateContent(renderFn(rows), spec.budget);
41829
- renderedFiles.set(filename, content);
41830
- entityFileHashes[filename] = { hash: contentHash(content) };
41831
- const filePath = (0, import_node_path5.join)(entityDir, filename);
41832
- if (atomicWrite(filePath, content)) {
41833
- filesWritten.push(filePath);
41834
- } else {
41835
- counters.skipped++;
41836
- }
41837
- }
41838
- const fileKeys = Object.keys(def.files);
41839
- const effectiveCombined = def.combined ?? (fileKeys.length > 1 && renderedFiles.size > 1 ? (
41840
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41841
- { outputFile: fileKeys[0] }
41842
- ) : void 0);
41843
- if (effectiveCombined && renderedFiles.size > 0) {
41844
- const excluded = new Set(effectiveCombined.exclude ?? []);
41845
- const parts = [];
41846
- for (const filename of Object.keys(def.files)) {
41847
- if (!excluded.has(filename) && renderedFiles.has(filename)) {
41848
- parts.push(renderedFiles.get(filename) ?? "");
41849
- }
41850
- }
41851
- if (parts.length > 0) {
41852
- const combinedContent = parts.join("\n\n---\n\n");
41853
- const combinedPath = (0, import_node_path5.join)(entityDir, effectiveCombined.outputFile);
41854
- if (atomicWrite(combinedPath, combinedContent)) {
41855
- filesWritten.push(combinedPath);
41956
+ const renderedFiles = /* @__PURE__ */ new Map();
41957
+ const entityFileHashes = {};
41958
+ const protection = protectedTables.size > 0 ? { protectedTables, currentTable: table } : void 0;
41959
+ for (const [filename, spec] of Object.entries(def.files)) {
41960
+ if (signal?.aborted) return null;
41961
+ const mergeDefaults = def.sourceDefaults && spec.source.type !== "self" && spec.source.type !== "custom" && spec.source.type !== "enriched";
41962
+ const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
41963
+ const rows = await resolveEntitySource(
41964
+ source,
41965
+ entityRow,
41966
+ entityPk,
41967
+ this._adapter,
41968
+ protection
41969
+ );
41970
+ if (spec.omitIfEmpty && rows.length === 0) continue;
41971
+ const renderFn = compileEntityRender(spec.render);
41972
+ const content = truncateContent(renderFn(rows), spec.budget);
41973
+ renderedFiles.set(filename, content);
41974
+ entityFileHashes[filename] = { hash: contentHash(content) };
41975
+ const filePath = (0, import_node_path5.join)(entityDir, filename);
41976
+ if (atomicWrite(filePath, content)) {
41977
+ filesWritten.push(filePath);
41856
41978
  } else {
41857
41979
  counters.skipped++;
41858
41980
  }
41859
- renderedFiles.set(effectiveCombined.outputFile, combinedContent);
41860
- entityFileHashes[effectiveCombined.outputFile] = { hash: contentHash(combinedContent) };
41861
41981
  }
41982
+ const fileKeys = Object.keys(def.files);
41983
+ const effectiveCombined = def.combined ?? (fileKeys.length > 1 && renderedFiles.size > 1 ? (
41984
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41985
+ { outputFile: fileKeys[0] }
41986
+ ) : void 0);
41987
+ if (effectiveCombined && renderedFiles.size > 0) {
41988
+ const excluded = new Set(effectiveCombined.exclude ?? []);
41989
+ const parts = [];
41990
+ for (const filename of Object.keys(def.files)) {
41991
+ if (!excluded.has(filename) && renderedFiles.has(filename)) {
41992
+ parts.push(renderedFiles.get(filename) ?? "");
41993
+ }
41994
+ }
41995
+ if (parts.length > 0) {
41996
+ const combinedContent = parts.join("\n\n---\n\n");
41997
+ const combinedPath = (0, import_node_path5.join)(entityDir, effectiveCombined.outputFile);
41998
+ if (atomicWrite(combinedPath, combinedContent)) {
41999
+ filesWritten.push(combinedPath);
42000
+ } else {
42001
+ counters.skipped++;
42002
+ }
42003
+ renderedFiles.set(effectiveCombined.outputFile, combinedContent);
42004
+ entityFileHashes[effectiveCombined.outputFile] = {
42005
+ hash: contentHash(combinedContent)
42006
+ };
42007
+ }
42008
+ }
42009
+ manifestEntry.entities[slug] = entityFileHashes;
42010
+ const entitiesRendered = i6 + 1;
42011
+ throttle.tick({
42012
+ kind: "table-progress",
42013
+ table,
42014
+ entitiesRendered,
42015
+ entitiesTotal,
42016
+ tableIndex,
42017
+ tableCount,
42018
+ pct: entitiesTotal > 0 ? entitiesRendered / entitiesTotal * 100 : 100
42019
+ });
41862
42020
  }
41863
- manifestEntry.entities[slug] = entityFileHashes;
42021
+ throttle.force({
42022
+ kind: "table-done",
42023
+ table,
42024
+ entitiesRendered: entitiesTotal,
42025
+ entitiesTotal,
42026
+ tableIndex,
42027
+ tableCount,
42028
+ pct: 100
42029
+ });
42030
+ return manifestEntry;
41864
42031
  }
41865
- manifestData[table] = manifestEntry;
42032
+ );
42033
+ if (signal?.aborted) return null;
42034
+ const manifestData = {};
42035
+ for (let i6 = 0; i6 < renderedEntries.length; i6++) {
42036
+ const entry = renderedEntries[i6];
42037
+ if (entry == null) return null;
42038
+ manifestData[entityTables[i6][0]] = entry;
41866
42039
  }
41867
42040
  return manifestData;
41868
42041
  }
@@ -43114,14 +43287,27 @@ function buildParsedConfig(raw, sourceName, configDir2) {
43114
43287
  const entityContexts = parseEntityContexts(config.entityContexts);
43115
43288
  return name !== void 0 ? { dbPath, name, tables, entityContexts } : { dbPath, tables, entityContexts };
43116
43289
  }
43290
+ function isDbRefShaped(raw) {
43291
+ return /^\s*\$\{LATTICE_DB:/.test(raw);
43292
+ }
43293
+ function parseDbRef(raw) {
43294
+ const m4 = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(raw.trim());
43295
+ return m4 ? { label: m4[1] ?? "" } : null;
43296
+ }
43117
43297
  function resolveDbPath(raw, configDir2) {
43118
- const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(raw.trim());
43119
- if (labelMatch) {
43120
- const label = labelMatch[1] ?? "";
43121
- const url = getDbCredential(label);
43298
+ if (isDbRefShaped(raw)) {
43299
+ const ref = parseDbRef(raw);
43300
+ if (!ref) {
43301
+ throw new Error(
43302
+ `Lattice: malformed \${LATTICE_DB:\u2026} reference ${JSON.stringify(
43303
+ raw.trim()
43304
+ )} \u2014 the label may contain only [A-Za-z0-9._-] (no spaces). This usually means a workspace was created with an unsanitized name.`
43305
+ );
43306
+ }
43307
+ const url = getDbCredential(ref.label);
43122
43308
  if (!url) {
43123
43309
  throw new Error(
43124
- `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}.`
43310
+ `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}.`
43125
43311
  );
43126
43312
  }
43127
43313
  return url;
@@ -43129,6 +43315,13 @@ function resolveDbPath(raw, configDir2) {
43129
43315
  if (/^postgres(ql)?:\/\//i.test(raw) || raw.startsWith("file:") || raw === ":memory:") {
43130
43316
  return raw;
43131
43317
  }
43318
+ if (raw.includes("${")) {
43319
+ throw new Error(
43320
+ `Lattice: refusing to treat ${JSON.stringify(
43321
+ raw.trim()
43322
+ )} as a database path \u2014 it looks like a malformed variable reference, not a file path.`
43323
+ );
43324
+ }
43132
43325
  return (0, import_node_path11.resolve)(configDir2, raw);
43133
43326
  }
43134
43327
  var warnedDeprecatedRefs = /* @__PURE__ */ new Set();
@@ -44461,6 +44654,54 @@ var Lattice = class _Lattice {
44461
44654
  async insert(table, row, provenance) {
44462
44655
  const notInit = this._notInitError();
44463
44656
  if (notInit) return notInit;
44657
+ const { sql, values, pkValue, rowWithPk } = this._prepareInsert(table, row);
44658
+ await runAsyncOrSync(this._adapter, sql, values);
44659
+ await this._afterInsert(table, pkValue, rowWithPk, provenance);
44660
+ return pkValue;
44661
+ }
44662
+ /**
44663
+ * Insert a row while atomically forcing its cloud row-visibility, regardless of
44664
+ * the table's `default_row_visibility`. The per-table insert trigger reads a
44665
+ * transaction-local GUC (`lattice.force_row_visibility`); we set it and run the
44666
+ * INSERT inside a single transaction, so the row is stamped at `visibility` the
44667
+ * instant it exists — it is never momentarily visible at the table default, and
44668
+ * the change-feed `NOTIFY` (delivered only at COMMIT) fires when the row already
44669
+ * carries this visibility. This closes the create-then-demote window that a
44670
+ * plain `insert()` + `setRowVisibility()` would leave open.
44671
+ *
44672
+ * Postgres-only: SQLite is single-user (no cross-viewer leak) and has no trigger
44673
+ * to read the GUC, so it degrades to a plain {@link insert}. A `never_share`
44674
+ * table still wins — its rows are forced private even if `visibility` is
44675
+ * `'everyone'` (the trigger enforces that precedence).
44676
+ *
44677
+ * @since 3.1.0
44678
+ */
44679
+ async insertForcingVisibility(table, row, visibility, provenance) {
44680
+ const notInit = this._notInitError();
44681
+ if (notInit) return notInit;
44682
+ const vis = visibility;
44683
+ if (vis !== "private" && vis !== "everyone") {
44684
+ throw new Error(`lattice: invalid forced visibility "${vis}"`);
44685
+ }
44686
+ const withClient = this._adapter.withClient?.bind(this._adapter);
44687
+ if (this.getDialect() !== "postgres" || !withClient) {
44688
+ return this.insert(table, row, provenance);
44689
+ }
44690
+ const { sql, values, pkValue, rowWithPk } = this._prepareInsert(table, row);
44691
+ await withClient(async (tx) => {
44692
+ await tx.run(`SELECT set_config('lattice.force_row_visibility', ?, true)`, [visibility]);
44693
+ await tx.run(sql, values);
44694
+ });
44695
+ await this._afterInsert(table, pkValue, rowWithPk, provenance);
44696
+ return pkValue;
44697
+ }
44698
+ /**
44699
+ * Build the INSERT statement + canonical pk for a row (sanitize → schema-filter →
44700
+ * auto-pk → encrypt). Shared by {@link insert} and {@link insertForcingVisibility}
44701
+ * so both produce byte-identical writes; the latter only differs in running it
44702
+ * inside a GUC-scoped transaction.
44703
+ */
44704
+ _prepareInsert(table, row) {
44464
44705
  this._assertIdent(table);
44465
44706
  const sanitized = this._filterToSchemaColumns(table, this._sanitizer.sanitizeRow(row));
44466
44707
  const pkCols = this._schema.getPrimaryKey(table);
@@ -44476,12 +44717,17 @@ var Lattice = class _Lattice {
44476
44717
  const cols = Object.keys(encrypted).map((c6) => `"${c6}"`).join(", ");
44477
44718
  const placeholders = Object.keys(encrypted).map(() => "?").join(", ");
44478
44719
  const values = Object.values(encrypted);
44479
- await runAsyncOrSync(
44480
- this._adapter,
44481
- `INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`,
44482
- values
44483
- );
44484
44720
  const pkValue = this._serializeRowPk(table, rowWithPk);
44721
+ return {
44722
+ sql: `INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`,
44723
+ values,
44724
+ pkValue,
44725
+ rowWithPk
44726
+ };
44727
+ }
44728
+ /** Post-insert side effects (changelog, audit, write hooks, embedding sync),
44729
+ * identical for the plain and force-visibility insert paths. */
44730
+ async _afterInsert(table, pkValue, rowWithPk, provenance) {
44485
44731
  await this._appendChangelog(
44486
44732
  table,
44487
44733
  pkValue,
@@ -44495,7 +44741,6 @@ var Lattice = class _Lattice {
44495
44741
  this._sanitizer.emitAudit(table, "insert", pkValue);
44496
44742
  await this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
44497
44743
  this._syncEmbedding(table, "insert", rowWithPk, pkValue);
44498
- return pkValue;
44499
44744
  }
44500
44745
  /**
44501
44746
  * Insert a row and return the full inserted row (including auto-generated
@@ -44562,6 +44807,7 @@ var Lattice = class _Lattice {
44562
44807
  const sanitized = this._filterToSchemaColumns(table, this._sanitizer.sanitizeRow(row));
44563
44808
  const encrypted = this._encryptRow(table, sanitized);
44564
44809
  const setCols = Object.keys(encrypted).map((c6) => `"${c6}" = ?`).join(", ");
44810
+ if (setCols === "") return;
44565
44811
  const { clause, params: pkParams } = this._pkWhere(table, id);
44566
44812
  let previousValues = null;
44567
44813
  if (this._changelogTables.has(table)) {
@@ -45178,13 +45424,32 @@ var Lattice = class _Lattice {
45178
45424
  // -------------------------------------------------------------------------
45179
45425
  // Sync
45180
45426
  // -------------------------------------------------------------------------
45181
- async render(outputDir) {
45427
+ async render(outputDir, opts = {}) {
45182
45428
  const notInit = this._notInitError();
45183
45429
  if (notInit) return notInit;
45184
- const result = await this._render.render(outputDir);
45430
+ const result = await this._render.render(outputDir, opts);
45185
45431
  for (const h6 of this._renderHandlers) h6(result);
45186
45432
  return result;
45187
45433
  }
45434
+ /**
45435
+ * Render into `outputDir` through the shared single-flight guard, intended to
45436
+ * be called fire-and-forget (e.g. the GUI's instant-open background render).
45437
+ *
45438
+ * The guard ({@link _renderGuarded}) holds {@link _autoRenderInFlight} for the
45439
+ * render's duration, so a data mutation that lands while this render is in
45440
+ * flight is deferred by {@link _runAutoRender} and coalesced — when this
45441
+ * render settles, `finally` clears the flag and re-arms exactly one follow-up
45442
+ * render via {@link _rearmAutoRenderIfPending}. Net invariant: at most one
45443
+ * render to a given dir at a time.
45444
+ *
45445
+ * Errors propagate to the caller (the GUI surfaces them, never silently swallowed); they are
45446
+ * not swallowed here.
45447
+ */
45448
+ async renderInBackground(outputDir, opts = {}) {
45449
+ const notInit = this._notInitError();
45450
+ if (notInit) return notInit;
45451
+ return this._renderGuarded(outputDir, opts);
45452
+ }
45188
45453
  async sync(outputDir) {
45189
45454
  const notInit = this._notInitError();
45190
45455
  if (notInit) return notInit;
@@ -45526,6 +45791,30 @@ var Lattice = class _Lattice {
45526
45791
  }, this._autoRenderDebounceMs);
45527
45792
  this._autoRenderTimer.unref();
45528
45793
  }
45794
+ /**
45795
+ * Shared single-flight render path used by {@link renderInBackground}.
45796
+ *
45797
+ * Holds {@link _autoRenderInFlight} for the render's duration so the
45798
+ * mutation-driven {@link _runAutoRender} defers while this render runs (it
45799
+ * sees the flag and marks itself pending instead of starting a second,
45800
+ * overlapping render). On settle, `finally` clears the flag and re-arms a
45801
+ * single coalesced follow-up render if any mutation arrived mid-flight.
45802
+ * Errors propagate to the caller; the flag is always cleared.
45803
+ */
45804
+ async _renderGuarded(outputDir, opts) {
45805
+ while (this._autoRenderInFlight) {
45806
+ await new Promise((r6) => setImmediate(r6));
45807
+ }
45808
+ this._autoRenderInFlight = true;
45809
+ try {
45810
+ const result = await this._render.render(outputDir, opts);
45811
+ for (const h6 of this._renderHandlers) h6(result);
45812
+ return result;
45813
+ } finally {
45814
+ this._autoRenderInFlight = false;
45815
+ this._rearmAutoRenderIfPending();
45816
+ }
45817
+ }
45529
45818
  async _runAutoRender() {
45530
45819
  const dir = this._autoRenderDir;
45531
45820
  if (!dir || !this._initialized) return;
@@ -47086,7 +47375,7 @@ function archiveLocalSqlite(dbPath) {
47086
47375
  async function cloudRlsInstalled(probe) {
47087
47376
  const row = await getAsyncOrSync(
47088
47377
  probe.adapter,
47089
- `SELECT to_regclass('public.__lattice_owners') AS reg`
47378
+ `SELECT to_regclass('__lattice_owners') AS reg`
47090
47379
  );
47091
47380
  return !!row && row.reg != null;
47092
47381
  }
@@ -47143,6 +47432,19 @@ function isPostgresUrl(url) {
47143
47432
  }
47144
47433
 
47145
47434
  // src/cloud/rls.ts
47435
+ async function runCloudBootstrapSql(db, sql) {
47436
+ const adapter = db.adapter;
47437
+ if (adapter.withClient) {
47438
+ await adapter.withClient(async (tx) => {
47439
+ await tx.run("SELECT pg_advisory_xact_lock($1::bigint)", [
47440
+ LATTICE_MIGRATION_LOCK_ID.toString()
47441
+ ]);
47442
+ await tx.run(sql);
47443
+ });
47444
+ } else {
47445
+ await runAsyncOrSync(adapter, sql);
47446
+ }
47447
+ }
47146
47448
  function isPg(db) {
47147
47449
  return db.getDialect() === "postgres";
47148
47450
  }
@@ -47153,6 +47455,31 @@ function pkSqlExpr(pkCols, prefix) {
47153
47455
  return pkCols.map((c6) => `CAST(${prefix}"${c6}" AS TEXT)`).join(` || chr(9) || `);
47154
47456
  }
47155
47457
  var MEMBER_GROUP = "lattice_members";
47458
+ function pinDefinerSearchPath(sql, schema) {
47459
+ const safe = schema.replace(/"/g, '""');
47460
+ return sql.replace(
47461
+ /SECURITY DEFINER AS/g,
47462
+ `SECURITY DEFINER SET search_path = "${safe}", pg_temp AS`
47463
+ );
47464
+ }
47465
+ async function cloudSchema(db) {
47466
+ const row = await getAsyncOrSync(db.adapter, `SELECT current_schema() AS schema`);
47467
+ const s2 = row?.schema;
47468
+ if (typeof s2 !== "string" || s2.length === 0) {
47469
+ throw new Error("cloud RLS: could not resolve current_schema() for search_path pinning");
47470
+ }
47471
+ return s2;
47472
+ }
47473
+ function revokeSchemaCreateSql(schema) {
47474
+ const lit = `'${schema.replace(/'/g, "''")}'`;
47475
+ return `
47476
+ DO $LATTICE_REVOKE$ BEGIN
47477
+ EXECUTE format('REVOKE CREATE ON SCHEMA %I FROM PUBLIC', ${lit});
47478
+ EXCEPTION WHEN OTHERS THEN
47479
+ NULL; -- not the schema owner, or already revoked
47480
+ END $LATTICE_REVOKE$;
47481
+ `;
47482
+ }
47156
47483
  var CLOUD_RLS_BOOTSTRAP_SQL = `
47157
47484
  -- Member group (NOLOGIN). Members inherit schema/connect/table privileges from it;
47158
47485
  -- RLS filters per the individual member's login role, so the group never widens
@@ -47212,6 +47539,82 @@ CREATE TABLE IF NOT EXISTS "__lattice_cell_grants" (
47212
47539
  PRIMARY KEY ("table_name", "pk", "column_name", "grantee_role")
47213
47540
  );
47214
47541
 
47542
+ -- Per-table policy: the owner-controlled defaults that govern a whole table.
47543
+ -- default_row_visibility is the visibility NEW rows are stamped with (the insert
47544
+ -- trigger reads it); never_share is a hard exclusion \u2014 the share/grant functions
47545
+ -- refuse to elevate such a table and the trigger forces its rows private. Owner-
47546
+ -- managed; members have no grant (it never appears in their data API).
47547
+ CREATE TABLE IF NOT EXISTS "__lattice_table_policy" (
47548
+ "table_name" text PRIMARY KEY,
47549
+ "default_row_visibility" text NOT NULL DEFAULT 'private'
47550
+ CHECK ("default_row_visibility" IN ('private','everyone')),
47551
+ "never_share" boolean NOT NULL DEFAULT false,
47552
+ "updated_by" text NOT NULL DEFAULT session_user,
47553
+ "updated_at" timestamptz NOT NULL DEFAULT now()
47554
+ );
47555
+
47556
+ -- Per-column audience policy: the CANONICAL store of which column carries which
47557
+ -- audience spec (role: / subject: / source: / owner / everyone). Previously the
47558
+ -- spec lived only in the owner's on-disk YAML and was compiled into the mask view
47559
+ -- once at init; storing it here makes it cloud-canonical and member-consistent.
47560
+ -- The generated <table>_v mask view is regenerated from THIS table on change.
47561
+ -- Owner-managed; members have no grant.
47562
+ CREATE TABLE IF NOT EXISTS "__lattice_column_policy" (
47563
+ "table_name" text NOT NULL,
47564
+ "column_name" text NOT NULL,
47565
+ "audience" text NOT NULL,
47566
+ "updated_by" text NOT NULL DEFAULT session_user,
47567
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
47568
+ PRIMARY KEY ("table_name", "column_name")
47569
+ );
47570
+
47571
+ -- Owner-only audit of issued member invites: which scoped role was minted for
47572
+ -- which email (HASHED \u2014 the plaintext email is never stored), when it expires,
47573
+ -- and whether it was redeemed/revoked. No plaintext password is ever stored
47574
+ -- (the credential lives only inside the email-bound token the owner delivers).
47575
+ -- Owner-managed; members have no grant. Named distinctly from any legacy
47576
+ -- team-model invitations table so a pre-existing cloud never collides.
47577
+ CREATE TABLE IF NOT EXISTS "__lattice_member_invites" (
47578
+ "id" text PRIMARY KEY,
47579
+ "role" text NOT NULL,
47580
+ "email_hash" text NOT NULL,
47581
+ "email" text,
47582
+ "created_by" text NOT NULL DEFAULT session_user,
47583
+ "created_at" timestamptz NOT NULL DEFAULT now(),
47584
+ "expires_at" timestamptz NOT NULL,
47585
+ "redeemed_at" timestamptz,
47586
+ "revoked_at" timestamptz
47587
+ );
47588
+ -- Plaintext invitee email (owner-only table; members have no grant) so the
47589
+ -- owner's Members list can show who each member is. Added via ALTER so clouds
47590
+ -- created before this column converge to it on the owner's next open (the
47591
+ -- bootstrap is now run directly + idempotently, not version-gated).
47592
+ ALTER TABLE "__lattice_member_invites" ADD COLUMN IF NOT EXISTS "email" text;
47593
+
47594
+ -- #3.1 \u2014 one-time-use + revocation enforcement. After a member authenticates to
47595
+ -- the cloud with their minted credential, the join path calls this to CLAIM the
47596
+ -- invite. The single atomic UPDATE stamps redeemed_at and returns true ONLY when
47597
+ -- an invite for the CALLING role (session_user) is still pending: not already
47598
+ -- redeemed (one-time-use), not revoked, and not expired. A replayed redeem of a
47599
+ -- leaked token, a revoked invite, or an expired one returns false, so the caller
47600
+ -- rejects the join. Members have no direct grant on the owner-only
47601
+ -- __lattice_member_invites table \u2014 this SECURITY DEFINER function is the only
47602
+ -- path, and it can claim ONLY the caller's own invite (keyed on session_user,
47603
+ -- never a caller-supplied parameter, so one member can't burn another's invite).
47604
+ CREATE OR REPLACE FUNCTION lattice_claim_invite()
47605
+ RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER AS $fn$
47606
+ DECLARE v_ok boolean;
47607
+ BEGIN
47608
+ UPDATE "__lattice_member_invites"
47609
+ SET "redeemed_at" = now()
47610
+ WHERE "role" = session_user
47611
+ AND "redeemed_at" IS NULL
47612
+ AND "revoked_at" IS NULL
47613
+ AND "expires_at" > now()
47614
+ RETURNING true INTO v_ok;
47615
+ RETURN COALESCE(v_ok, false);
47616
+ END $fn$;
47617
+
47215
47618
  -- Visibility check. SECURITY DEFINER so it reads bookkeeping the member can't;
47216
47619
  -- keyed on session_user (the member's login role). A row with no ownership record
47217
47620
  -- is visible to nobody.
@@ -47237,6 +47640,10 @@ BEGIN
47237
47640
  IF p_visibility NOT IN ('private','everyone','custom') THEN
47238
47641
  RAISE EXCEPTION 'lattice: invalid visibility %', p_visibility;
47239
47642
  END IF;
47643
+ IF p_visibility <> 'private'
47644
+ AND COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = p_table), false) THEN
47645
+ RAISE EXCEPTION 'lattice: "%" is a private-only table and cannot be shared', p_table;
47646
+ END IF;
47240
47647
  SELECT o."owner_role" INTO v_owner FROM "__lattice_owners" o
47241
47648
  WHERE o."table_name" = p_table AND o."pk" = p_pk;
47242
47649
  IF v_owner IS NULL THEN RAISE EXCEPTION 'lattice: no ownership record for %/%', p_table, p_pk; END IF;
@@ -47250,6 +47657,9 @@ CREATE OR REPLACE FUNCTION lattice_grant_row(p_table text, p_pk text, p_grantee
47250
47657
  RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
47251
47658
  DECLARE v_owner text;
47252
47659
  BEGIN
47660
+ IF COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = p_table), false) THEN
47661
+ RAISE EXCEPTION 'lattice: "%" is a private-only table and cannot be shared', p_table;
47662
+ END IF;
47253
47663
  SELECT o."owner_role" INTO v_owner FROM "__lattice_owners" o
47254
47664
  WHERE o."table_name" = p_table AND o."pk" = p_pk;
47255
47665
  IF v_owner IS NULL THEN RAISE EXCEPTION 'lattice: no ownership record for %/%', p_table, p_pk; END IF;
@@ -47339,6 +47749,9 @@ CREATE OR REPLACE FUNCTION lattice_grant_cell(p_table text, p_pk text, p_column
47339
47749
  RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
47340
47750
  DECLARE v_owner text;
47341
47751
  BEGIN
47752
+ IF COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = p_table), false) THEN
47753
+ RAISE EXCEPTION 'lattice: "%" is a private-only table and cannot be shared', p_table;
47754
+ END IF;
47342
47755
  SELECT o."owner_role" INTO v_owner FROM "__lattice_owners" o
47343
47756
  WHERE o."table_name" = p_table AND o."pk" = p_pk;
47344
47757
  IF v_owner IS NULL OR v_owner <> session_user THEN
@@ -47371,6 +47784,87 @@ RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
47371
47784
  SELECT lattice_row_visible('files', p_source_ref)
47372
47785
  $fn$;
47373
47786
 
47787
+ -- Is the connected member the OWNER of this row? Used by the "owner" column
47788
+ -- audience (a secret column reveals only to the row owner). SECURITY DEFINER +
47789
+ -- session_user, like the other predicates.
47790
+ CREATE OR REPLACE FUNCTION lattice_is_owner(p_table text, p_pk text)
47791
+ RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
47792
+ SELECT EXISTS (
47793
+ SELECT 1 FROM "__lattice_owners" o
47794
+ WHERE o."table_name" = p_table AND o."pk" = p_pk AND o."owner_role" = session_user
47795
+ )
47796
+ $fn$;
47797
+
47798
+ -- Owner-only: set a table's default row visibility for NEW rows. Raises unless the
47799
+ -- caller can create roles (a cloud owner / DBA), like lattice_assign_role. Rejects
47800
+ -- 'everyone' on a never-share table.
47801
+ CREATE OR REPLACE FUNCTION lattice_set_table_default_visibility(p_table text, p_visibility text)
47802
+ RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
47803
+ BEGIN
47804
+ IF NOT (SELECT rolcreaterole FROM pg_roles WHERE rolname = session_user) THEN
47805
+ RAISE EXCEPTION 'lattice: only a cloud owner may set a table''s default visibility';
47806
+ END IF;
47807
+ IF p_visibility NOT IN ('private','everyone') THEN
47808
+ RAISE EXCEPTION 'lattice: invalid default visibility %', p_visibility;
47809
+ END IF;
47810
+ IF p_visibility = 'everyone'
47811
+ AND COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = p_table), false) THEN
47812
+ RAISE EXCEPTION 'lattice: "%" is a private-only table; its rows cannot default to everyone', p_table;
47813
+ END IF;
47814
+ INSERT INTO "__lattice_table_policy" ("table_name","default_row_visibility","updated_by","updated_at")
47815
+ VALUES (p_table, p_visibility, session_user, now())
47816
+ ON CONFLICT ("table_name") DO UPDATE
47817
+ SET "default_row_visibility" = EXCLUDED."default_row_visibility",
47818
+ "updated_by" = session_user, "updated_at" = now();
47819
+ END $fn$;
47820
+
47821
+ -- Owner-only: mark a table never-shareable (Secrets/Messages-class). When true the
47822
+ -- share/grant functions raise and the insert trigger forces new rows private; the
47823
+ -- default visibility is also forced private. Turning it ON also RETROACTIVELY
47824
+ -- privatizes the table: any row currently shared ('everyone'/'custom') is reset to
47825
+ -- 'private' and every existing row/cell grant on the table is dropped \u2014 otherwise
47826
+ -- flagging a table never-share would leave already-leaked rows visible, defeating
47827
+ -- the point. Idempotent: re-running with already-private rows updates nothing.
47828
+ CREATE OR REPLACE FUNCTION lattice_set_table_never_share(p_table text, p_on boolean)
47829
+ RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
47830
+ BEGIN
47831
+ IF NOT (SELECT rolcreaterole FROM pg_roles WHERE rolname = session_user) THEN
47832
+ RAISE EXCEPTION 'lattice: only a cloud owner may change a table''s never-share flag';
47833
+ END IF;
47834
+ INSERT INTO "__lattice_table_policy" ("table_name","never_share","default_row_visibility","updated_by","updated_at")
47835
+ VALUES (p_table, p_on, CASE WHEN p_on THEN 'private' ELSE 'private' END, session_user, now())
47836
+ ON CONFLICT ("table_name") DO UPDATE
47837
+ SET "never_share" = EXCLUDED."never_share",
47838
+ "default_row_visibility" = CASE WHEN EXCLUDED."never_share"
47839
+ THEN 'private' ELSE "__lattice_table_policy"."default_row_visibility" END,
47840
+ "updated_by" = session_user, "updated_at" = now();
47841
+ IF p_on THEN
47842
+ UPDATE "__lattice_owners" SET "visibility" = 'private', "updated_at" = now()
47843
+ WHERE "table_name" = p_table AND "visibility" <> 'private';
47844
+ DELETE FROM "__lattice_row_grants" WHERE "table_name" = p_table;
47845
+ DELETE FROM "__lattice_cell_grants" WHERE "table_name" = p_table;
47846
+ END IF;
47847
+ END $fn$;
47848
+
47849
+ -- Owner-only: set (or clear) a column's audience spec in the canonical DB store.
47850
+ -- An empty/null spec removes the policy row (column becomes unmasked). The GUI/lib
47851
+ -- regenerates the table's mask view from this store after calling this.
47852
+ CREATE OR REPLACE FUNCTION lattice_set_column_audience(p_table text, p_column text, p_audience text)
47853
+ RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
47854
+ BEGIN
47855
+ IF NOT (SELECT rolcreaterole FROM pg_roles WHERE rolname = session_user) THEN
47856
+ RAISE EXCEPTION 'lattice: only a cloud owner may set a column audience';
47857
+ END IF;
47858
+ IF p_audience IS NULL OR btrim(p_audience) = '' THEN
47859
+ DELETE FROM "__lattice_column_policy" WHERE "table_name" = p_table AND "column_name" = p_column;
47860
+ ELSE
47861
+ INSERT INTO "__lattice_column_policy" ("table_name","column_name","audience","updated_by","updated_at")
47862
+ VALUES (p_table, p_column, p_audience, session_user, now())
47863
+ ON CONFLICT ("table_name","column_name") DO UPDATE
47864
+ SET "audience" = EXCLUDED."audience", "updated_by" = session_user, "updated_at" = now();
47865
+ END IF;
47866
+ END $fn$;
47867
+
47374
47868
  -- Append-only change feed. The per-table ownership trigger records one row per
47375
47869
  -- INSERT/UPDATE/DELETE; the AFTER INSERT trigger here fires pg_notify so a
47376
47870
  -- connected member's realtime broker refreshes. Members get no direct access \u2014
@@ -47402,6 +47896,62 @@ END $fn$;
47402
47896
  DROP TRIGGER IF EXISTS "lattice_notify_change_trg" ON "__lattice_changes";
47403
47897
  CREATE TRIGGER "lattice_notify_change_trg" AFTER INSERT ON "__lattice_changes"
47404
47898
  FOR EACH ROW EXECUTE FUNCTION lattice_notify_change();
47899
+
47900
+ -- #4.4 \u2014 seq-based catch-up after a realtime gap. NOTIFY is fire-and-forget, so a
47901
+ -- broker that drops its LISTEN (network blip, laptop sleep) misses every change
47902
+ -- during the gap. The broker tracks the highest seq it delivered and, on
47903
+ -- reconnect, replays what it missed via this function. Members have NO direct
47904
+ -- grant on __lattice_changes (reading it raw would leak every change on the
47905
+ -- cloud), so this SECURITY DEFINER function is the only path and it returns ONLY
47906
+ -- the rows the CALLING role can see: keyed on session_user via lattice_row_visible
47907
+ -- (same gate as live fan-out, #4.3). Deletes are excluded \u2014 the ownership record
47908
+ -- is gone post-delete so visibility can't be verified, and replaying them would
47909
+ -- leak deleted-row pks (the client reconciles deletes on its reconnect refetch).
47910
+ -- Bounded (LIMIT clamped \u2264 1000) so a long gap can't stream the whole table (Rule:
47911
+ -- bounded reads on a hot path).
47912
+ CREATE OR REPLACE FUNCTION lattice_changes_since(p_seq bigint, p_limit int)
47913
+ RETURNS TABLE(seq bigint, table_name text, pk text, op text, owner_role text, created_at timestamptz)
47914
+ LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
47915
+ SELECT c."seq", c."table_name", c."pk", c."op", c."owner_role", c."created_at"
47916
+ FROM "__lattice_changes" c
47917
+ WHERE c."seq" > p_seq
47918
+ AND c."op" = 'upsert'
47919
+ AND lattice_row_visible(c."table_name", c."pk")
47920
+ ORDER BY c."seq" ASC
47921
+ LIMIT GREATEST(0, LEAST(COALESCE(p_limit, 500), 1000));
47922
+ $fn$;
47923
+
47924
+ -- #2.1 \u2014 per-row access summary for the connecting role. The GUI attaches this as
47925
+ -- each row's _access so the sharing affordance renders, but __lattice_owners is
47926
+ -- owner-only bookkeeping (members have no grant), so a member reading it directly
47927
+ -- got "permission denied". This SECURITY DEFINER function returns visibility +
47928
+ -- whether the CALLER owns the row, ONLY for the rows the caller can actually see
47929
+ -- (lattice_row_visible, keyed on session_user) \u2014 so a member learns nothing about
47930
+ -- rows hidden from it. Member-callable; the owner gets the same view of its rows.
47931
+ CREATE OR REPLACE FUNCTION lattice_rows_access(p_table text, p_pks text[])
47932
+ RETURNS TABLE(pk text, visibility text, owned boolean)
47933
+ LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
47934
+ SELECT o."pk", o."visibility", (o."owner_role" = session_user) AS owned
47935
+ FROM "__lattice_owners" o
47936
+ WHERE o."table_name" = p_table
47937
+ AND o."pk" = ANY(p_pks)
47938
+ AND lattice_row_visible(o."table_name", o."pk");
47939
+ $fn$;
47940
+
47941
+ -- #2.1 \u2014 grantees of a CALLER-OWNED custom-shared row (who you shared YOUR row
47942
+ -- with). Only the row owner sees this (the WHERE pins owner_role = session_user),
47943
+ -- so a member can't enumerate another owner's grants. __lattice_row_grants is
47944
+ -- member-ungranted, so this SECURITY DEFINER function is the member-safe path.
47945
+ CREATE OR REPLACE FUNCTION lattice_row_grantees(p_table text, p_pks text[])
47946
+ RETURNS TABLE(pk text, grantee_role text)
47947
+ LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
47948
+ SELECT g."pk", g."grantee_role"
47949
+ FROM "__lattice_row_grants" g
47950
+ JOIN "__lattice_owners" o ON o."table_name" = g."table_name" AND o."pk" = g."pk"
47951
+ WHERE g."table_name" = p_table
47952
+ AND g."pk" = ANY(p_pks)
47953
+ AND o."owner_role" = session_user;
47954
+ $fn$;
47405
47955
  `;
47406
47956
  function tableRlsSql(table, pkCols) {
47407
47957
  const q3 = `"${table.replace(/"/g, '""')}"`;
@@ -47415,7 +47965,20 @@ CREATE OR REPLACE FUNCTION "${trg}"() RETURNS trigger LANGUAGE plpgsql SECURITY
47415
47965
  BEGIN
47416
47966
  IF TG_OP = 'INSERT' THEN
47417
47967
  INSERT INTO "__lattice_owners" ("table_name","pk","owner_role","visibility")
47418
- VALUES (${lit}, ${pkNew}, session_user, 'private')
47968
+ VALUES (${lit}, ${pkNew}, session_user,
47969
+ CASE
47970
+ -- never-share always wins: such a table's rows are private, full stop.
47971
+ WHEN COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = ${lit}), false)
47972
+ THEN 'private'
47973
+ -- per-INSERT override: a caller forcing visibility for THIS write (e.g.
47974
+ -- chat "private mode") sets the transaction-local lattice.force_row_visibility
47975
+ -- GUC, so the row is stamped atomically at insert \u2014 never momentarily at
47976
+ -- the table default, and the change-feed NOTIFY (deferred to COMMIT) only
47977
+ -- fires once the row already carries this visibility.
47978
+ WHEN NULLIF(current_setting('lattice.force_row_visibility', true), '') IN ('private','everyone')
47979
+ THEN current_setting('lattice.force_row_visibility', true)
47980
+ ELSE COALESCE((SELECT "default_row_visibility" FROM "__lattice_table_policy" WHERE "table_name" = ${lit}), 'private')
47981
+ END)
47419
47982
  ON CONFLICT ("table_name","pk") DO NOTHING;
47420
47983
  INSERT INTO "__lattice_changes" ("table_name","pk","op","owner_role")
47421
47984
  VALUES (${lit}, ${pkNew}, 'upsert', session_user);
@@ -47455,20 +48018,15 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
47455
48018
  }
47456
48019
  async function installCloudRls(db) {
47457
48020
  if (!isPg(db)) return;
47458
- const migration = {
47459
- // v3 added the audience helpers; v4 the role model; v5 the per-card override
47460
- // model (__lattice_cell_grants + lattice_cell_visible / lattice_grant_cell).
47461
- // The bootstrap is fully idempotent (CREATE OR REPLACE / IF NOT EXISTS).
47462
- version: "internal:cloud-rls:bootstrap:v5",
47463
- sql: CLOUD_RLS_BOOTSTRAP_SQL
47464
- };
47465
- await db.migrate([migration]);
48021
+ const schema = await cloudSchema(db);
48022
+ const sql = pinDefinerSearchPath(CLOUD_RLS_BOOTSTRAP_SQL, schema) + revokeSchemaCreateSql(schema);
48023
+ await runCloudBootstrapSql(db, sql);
47466
48024
  }
47467
48025
  async function enableChangelogRls(db) {
47468
48026
  if (!isPg(db)) return;
47469
- const migration = {
47470
- version: "internal:cloud-rls:changelog:v1",
47471
- sql: `
48027
+ await runCloudBootstrapSql(
48028
+ db,
48029
+ `
47472
48030
  ALTER TABLE "__lattice_changelog" ENABLE ROW LEVEL SECURITY;
47473
48031
  ALTER TABLE "__lattice_changelog" FORCE ROW LEVEL SECURITY;
47474
48032
  GRANT SELECT, INSERT ON "__lattice_changelog" TO ${MEMBER_GROUP};
@@ -47478,24 +48036,25 @@ CREATE POLICY "lattice_changelog_sel" ON "__lattice_changelog" FOR SELECT USING
47478
48036
  CASE
47479
48037
  WHEN "change_kind" = 'derived' THEN
47480
48038
  "source_ref" IS NOT NULL
48039
+ AND jsonb_array_length("source_ref"::jsonb) > 0
47481
48040
  AND NOT EXISTS (
47482
48041
  SELECT 1 FROM jsonb_array_elements_text("source_ref"::jsonb) AS src(sid)
47483
48042
  WHERE NOT lattice_source_visible(src.sid)
47484
48043
  )
47485
- ELSE lattice_row_visible("table_name", "row_id")
48044
+ ELSE lattice_is_owner("table_name", "row_id")
47486
48045
  END
47487
48046
  );
47488
48047
  DROP POLICY IF EXISTS "lattice_changelog_ins" ON "__lattice_changelog";
47489
48048
  CREATE POLICY "lattice_changelog_ins" ON "__lattice_changelog" FOR INSERT WITH CHECK (true);
47490
48049
  `
47491
- };
47492
- await db.migrate([migration]);
48050
+ );
47493
48051
  }
47494
48052
  async function enableRlsForTable(db, table, pkCols) {
47495
48053
  if (!isPg(db)) return;
48054
+ const schema = await cloudSchema(db);
47496
48055
  const migration = {
47497
- version: `internal:cloud-rls:table:${table}:v2`,
47498
- sql: tableRlsSql(table, pkCols)
48056
+ version: `internal:cloud-rls:table:${table}:v3`,
48057
+ sql: pinDefinerSearchPath(tableRlsSql(table, pkCols), schema)
47499
48058
  };
47500
48059
  await db.migrate([migration]);
47501
48060
  }
@@ -47542,7 +48101,12 @@ async function provisionMemberRole(db, role, password) {
47542
48101
  IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${role}') THEN
47543
48102
  CREATE ROLE "${role}" LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE PASSWORD '${password}';
47544
48103
  ELSE
47545
- ALTER ROLE "${role}" WITH LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE PASSWORD '${password}';
48104
+ -- Re-invite of an EXISTING role: set ONLY what changed (login + password).
48105
+ -- Restating NOSUPERUSER/superuser-class attrs trips Supabase supautils
48106
+ -- ("only superuser may alter the SUPERUSER attribute", 42501) since the
48107
+ -- owner 'postgres' isn't a true superuser. The role was already created
48108
+ -- NOSUPERUSER NOCREATEDB NOCREATEROLE, so there is nothing to restate.
48109
+ ALTER ROLE "${role}" WITH LOGIN PASSWORD '${password}';
47546
48110
  END IF;
47547
48111
  END $LATTICE$`
47548
48112
  );
@@ -47581,9 +48145,28 @@ async function revokeCell(db, table, pk, column, grantee) {
47581
48145
  async function revokeMemberRole(db, role) {
47582
48146
  assertPg(db);
47583
48147
  if (!ROLE_RE.test(role)) throw new Error(`lattice: invalid member role name "${role}"`);
47584
- await runAsyncOrSync(db.adapter, `DROP OWNED BY "${role}"`).catch(() => void 0);
48148
+ const exists = await getAsyncOrSync(
48149
+ db.adapter,
48150
+ `SELECT 1 AS x FROM pg_roles WHERE rolname = ?`,
48151
+ [role]
48152
+ );
48153
+ if (!exists) return;
48154
+ for (const stmt of [`REASSIGN OWNED BY "${role}" TO CURRENT_USER`, `DROP OWNED BY "${role}"`]) {
48155
+ try {
48156
+ await runAsyncOrSync(db.adapter, stmt);
48157
+ } catch (e6) {
48158
+ if (!isInsufficientPrivilege(e6)) throw e6;
48159
+ console.warn(
48160
+ `[cloud] "${stmt.split(" ").slice(0, 2).join(" ")} \u2026" skipped (insufficient privilege; a scoped member owns no objects): ${e6.message}`
48161
+ );
48162
+ }
48163
+ }
47585
48164
  await runAsyncOrSync(db.adapter, `DROP ROLE IF EXISTS "${role}"`);
47586
48165
  }
48166
+ function isInsufficientPrivilege(e6) {
48167
+ const err = e6 ?? {};
48168
+ return err.code === "42501" || /permission denied/i.test(err.message ?? "");
48169
+ }
47587
48170
 
47588
48171
  // src/cloud/discover.ts
47589
48172
  async function discoverCloudTables(db) {
@@ -47625,12 +48208,17 @@ function isRowAudience(audience) {
47625
48208
  const a6 = (audience ?? "").trim();
47626
48209
  return a6 === "" || a6 === "everyone" || a6 === "row-audience";
47627
48210
  }
47628
- function audiencePredicate(audience) {
48211
+ function audiencePredicate(audience, ctx) {
47629
48212
  if (isRowAudience(audience)) return "true";
47630
48213
  const clauses = audience.split("+").map((c6) => c6.trim()).filter(Boolean);
47631
48214
  const parts = [];
47632
48215
  for (const clause of clauses) {
47633
48216
  if (clause === "everyone" || clause === "row-audience") return "true";
48217
+ if (clause === "owner") {
48218
+ if (!ctx) throw new Error('lattice: the "owner" audience needs a row context');
48219
+ parts.push(`lattice_is_owner(${ctx.tableLit}, ${ctx.pkExpr})`);
48220
+ continue;
48221
+ }
47634
48222
  const idx = clause.indexOf(":");
47635
48223
  const kind = idx === -1 ? clause : clause.slice(0, idx);
47636
48224
  const arg = idx === -1 ? "" : clause.slice(idx + 1).trim();
@@ -47665,7 +48253,7 @@ function audienceViewSql(table, columns, pkCols, columnAudience) {
47665
48253
  const selectCols = columns.map((col) => {
47666
48254
  const aud = columnAudience[col] ?? "";
47667
48255
  if (isRowAudience(aud)) return quoteIdent(col);
47668
- const pred = audiencePredicate(aud);
48256
+ const pred = audiencePredicate(aud, { tableLit: lit, pkExpr });
47669
48257
  if (pred === "true") return quoteIdent(col);
47670
48258
  const colLit = `'${col.replace(/'/g, "''")}'`;
47671
48259
  const full = `(${pred}) OR lattice_cell_visible(${lit}, ${pkExpr}, ${colLit})`;
@@ -47700,6 +48288,92 @@ async function enableAudienceView(db, table, columns, pkCols, columnAudience) {
47700
48288
  };
47701
48289
  await db.migrate([migration]);
47702
48290
  }
48291
+ async function loadColumnPolicy(db, table) {
48292
+ if (db.getDialect() !== "postgres") return {};
48293
+ const rows = await allAsyncOrSync(
48294
+ db.adapter,
48295
+ `SELECT "column_name", "audience" FROM "__lattice_column_policy" WHERE "table_name" = ?`,
48296
+ [table]
48297
+ );
48298
+ const out = {};
48299
+ for (const r6 of rows) out[r6.column_name] = r6.audience;
48300
+ return out;
48301
+ }
48302
+ async function seedColumnPolicyFromYaml(db, table, yamlAudience) {
48303
+ if (db.getDialect() !== "postgres") return;
48304
+ const marker = `internal:cloud-column-seed:${table}:v1`;
48305
+ const already = await getAsyncOrSync(
48306
+ db.adapter,
48307
+ `SELECT 1 AS one FROM "__lattice_migrations" WHERE "version" = ?`,
48308
+ [marker]
48309
+ );
48310
+ if (already) return;
48311
+ for (const [col, aud] of Object.entries(yamlAudience)) {
48312
+ if (isRowAudience(aud)) continue;
48313
+ await runAsyncOrSync(
48314
+ db.adapter,
48315
+ `INSERT INTO "__lattice_column_policy" ("table_name","column_name","audience")
48316
+ VALUES (?, ?, ?) ON CONFLICT ("table_name","column_name") DO NOTHING`,
48317
+ [table, col, aud]
48318
+ );
48319
+ }
48320
+ await runAsyncOrSync(
48321
+ db.adapter,
48322
+ `INSERT INTO "__lattice_migrations" ("version","applied_at") VALUES (?, ?)
48323
+ ON CONFLICT ("version") DO NOTHING`,
48324
+ [marker, (/* @__PURE__ */ new Date()).toISOString()]
48325
+ );
48326
+ }
48327
+ async function regenerateAudienceViewFromDb(db, table, columns, pkCols) {
48328
+ if (db.getDialect() !== "postgres") return;
48329
+ if (pkCols.length === 0) return;
48330
+ const spec = await loadColumnPolicy(db, table);
48331
+ const view = quoteIdent(`${table}_v`);
48332
+ const base = quoteIdent(table);
48333
+ if (!tableNeedsAudienceView(spec)) {
48334
+ await runAsyncOrSync(
48335
+ db.adapter,
48336
+ `DROP VIEW IF EXISTS ${view};
48337
+ GRANT SELECT ON ${base} TO ${MEMBER_GROUP};`
48338
+ );
48339
+ return;
48340
+ }
48341
+ await runAsyncOrSync(db.adapter, audienceViewSql(table, columns, pkCols, spec));
48342
+ }
48343
+ async function setColumnAudience(db, table, column, audience, columns, pkCols) {
48344
+ if (db.getDialect() !== "postgres") return;
48345
+ await runAsyncOrSync(db.adapter, `SELECT lattice_set_column_audience(?, ?, ?)`, [
48346
+ table,
48347
+ column,
48348
+ audience
48349
+ ]);
48350
+ await regenerateAudienceViewFromDb(db, table, columns, pkCols);
48351
+ }
48352
+
48353
+ // src/cloud/table-policy.ts
48354
+ async function getTablePolicy(db, table) {
48355
+ if (db.getDialect() !== "postgres") return { defaultRowVisibility: "private", neverShare: false };
48356
+ const row = await getAsyncOrSync(
48357
+ db.adapter,
48358
+ `SELECT "default_row_visibility", "never_share" FROM "__lattice_table_policy" WHERE "table_name" = ?`,
48359
+ [table]
48360
+ );
48361
+ return {
48362
+ defaultRowVisibility: row?.default_row_visibility === "everyone" ? "everyone" : "private",
48363
+ neverShare: row?.never_share === true
48364
+ };
48365
+ }
48366
+ async function setTableDefaultVisibility(db, table, visibility) {
48367
+ if (db.getDialect() !== "postgres") return;
48368
+ await runAsyncOrSync(db.adapter, `SELECT lattice_set_table_default_visibility(?, ?)`, [
48369
+ table,
48370
+ visibility
48371
+ ]);
48372
+ }
48373
+ async function setTableNeverShare(db, table, on) {
48374
+ if (db.getDialect() !== "postgres") return;
48375
+ await runAsyncOrSync(db.adapter, `SELECT lattice_set_table_never_share(?, ?)`, [table, on]);
48376
+ }
47703
48377
 
47704
48378
  // src/cloud/fold-cache.ts
47705
48379
  function viewerSignature(viewer) {
@@ -47736,6 +48410,7 @@ var FoldCache = class {
47736
48410
  };
47737
48411
 
47738
48412
  // src/cloud/settings.ts
48413
+ var import_node_crypto14 = require("crypto");
47739
48414
  var CLOUD_SETTING_SYSTEM_PROMPT = "chat_system_prompt";
47740
48415
  var CLOUD_SETTINGS_BOOTSTRAP_SQL = `
47741
48416
  -- Owner-controlled, cloud-wide key/value settings. No grant to the member group,
@@ -47774,11 +48449,8 @@ END $fn$;
47774
48449
  `;
47775
48450
  async function installCloudSettings(db) {
47776
48451
  if (db.getDialect() !== "postgres") return;
47777
- const migration = {
47778
- version: "internal:cloud-settings:v1",
47779
- sql: CLOUD_SETTINGS_BOOTSTRAP_SQL
47780
- };
47781
- await db.migrate([migration]);
48452
+ const schema = await cloudSchema(db);
48453
+ await runCloudBootstrapSql(db, pinDefinerSearchPath(CLOUD_SETTINGS_BOOTSTRAP_SQL, schema));
47782
48454
  }
47783
48455
  async function getCloudSetting(db, key) {
47784
48456
  if (db.getDialect() !== "postgres") return null;
@@ -47797,23 +48469,39 @@ async function setCloudSetting(db, key, value) {
47797
48469
  }
47798
48470
 
47799
48471
  // src/cloud/setup.ts
48472
+ async function secureNewCloudTable(db, table, pk) {
48473
+ if (db.getDialect() !== "postgres") return;
48474
+ if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) return;
48475
+ if (pk.length === 0) return;
48476
+ await backfillOwnership(db, table, pk);
48477
+ await enableRlsForTable(db, table, pk);
48478
+ const cols = db.getRegisteredColumns(table);
48479
+ if (cols) {
48480
+ await seedColumnPolicyFromYaml(db, table, db.getColumnAudience(table));
48481
+ await regenerateAudienceViewFromDb(db, table, Object.keys(cols), pk);
48482
+ }
48483
+ }
47800
48484
  async function secureCloud(db) {
47801
48485
  if (db.getDialect() !== "postgres") return;
47802
48486
  await installCloudRls(db);
47803
48487
  await installCloudSettings(db);
47804
48488
  await db.ensureObservationSubstrate();
47805
48489
  await enableChangelogRls(db);
47806
- for (const table of db.getRegisteredTableNames()) {
47807
- if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) continue;
47808
- const pk = db.getPrimaryKey(table);
47809
- if (pk.length === 0) continue;
47810
- await backfillOwnership(db, table, pk);
47811
- await enableRlsForTable(db, table, pk);
47812
- const cols = db.getRegisteredColumns(table);
47813
- if (cols) {
47814
- await enableAudienceView(db, table, Object.keys(cols), pk, db.getColumnAudience(table));
47815
- }
48490
+ const registered = db.getRegisteredTableNames();
48491
+ for (const table of registered) {
48492
+ await secureNewCloudTable(db, table, db.getPrimaryKey(table));
48493
+ }
48494
+ if (registered.includes("secrets")) {
48495
+ await runAsyncOrSync(db.adapter, `SELECT lattice_set_table_never_share('secrets', true)`);
47816
48496
  }
48497
+ await runAsyncOrSync(
48498
+ db.adapter,
48499
+ `DO $LATTICE$ BEGIN
48500
+ IF to_regclass('__lattice_user_identity') IS NOT NULL THEN
48501
+ EXECUTE 'GRANT SELECT, INSERT, UPDATE ON "__lattice_user_identity" TO ${MEMBER_GROUP}';
48502
+ END IF;
48503
+ END $LATTICE$`
48504
+ );
47817
48505
  }
47818
48506
 
47819
48507
  // src/ai/llm-client.ts
@@ -48410,6 +49098,7 @@ function defaultPdfSender(auth) {
48410
49098
  NATIVE_ENTITY_NAMES,
48411
49099
  NATIVE_REGISTRY_TABLE,
48412
49100
  PostgresAdapter,
49101
+ ProgressThrottle,
48413
49102
  READ_ONLY_HEADER,
48414
49103
  ROOT_DIRNAME,
48415
49104
  ReferenceUnavailableError,
@@ -48473,6 +49162,7 @@ function defaultPdfSender(auth) {
48473
49162
  getCloudSetting,
48474
49163
  getDbCredential,
48475
49164
  getOrCreateMasterKey,
49165
+ getTablePolicy,
48476
49166
  getWorkspace,
48477
49167
  grantCell,
48478
49168
  hasFtsIndex,
@@ -48490,6 +49180,7 @@ function defaultPdfSender(auth) {
48490
49180
  listNativeBindings,
48491
49181
  listTokens,
48492
49182
  listWorkspaces,
49183
+ loadColumnPolicy,
48493
49184
  manifestPath,
48494
49185
  markdownTable,
48495
49186
  memberRoleName,
@@ -48517,6 +49208,7 @@ function defaultPdfSender(auth) {
48517
49208
  readToken,
48518
49209
  referenceLocalFile,
48519
49210
  referenceUrl,
49211
+ regenerateAudienceViewFromDb,
48520
49212
  registerNativeEntities,
48521
49213
  registryPath,
48522
49214
  resolveActiveS3Config,
@@ -48531,9 +49223,13 @@ function defaultPdfSender(auth) {
48531
49223
  saveDbCredentialForTeam,
48532
49224
  sealUnderSource,
48533
49225
  secureCloud,
49226
+ seedColumnPolicyFromYaml,
48534
49227
  setActiveWorkspace,
48535
49228
  setCloudSetting,
49229
+ setColumnAudience,
48536
49230
  setRowVisibility,
49231
+ setTableDefaultVisibility,
49232
+ setTableNeverShare,
48537
49233
  shredSource,
48538
49234
  slugify,
48539
49235
  summarizeText,