latticesql 3.2.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -49315,7 +49315,13 @@ var appJs = `
49315
49315
  var hash = location.hash || '#/';
49316
49316
  document.querySelectorAll('nav a').forEach(function (a) {
49317
49317
  var route = a.getAttribute('data-route') || a.getAttribute('href');
49318
- a.classList.toggle('active', route && hash.indexOf(route) === 0);
49318
+ // Match the route exactly or as a full path segment (route + '/...'),
49319
+ // never as a bare string prefix \u2014 otherwise '#/fs/files' would also
49320
+ // light up '#/fs/files_projects' (any sibling whose name starts with
49321
+ // the same word). The '/' boundary stops the prefix bleed while still
49322
+ // keeping a parent active on its own detail routes.
49323
+ var on = !!route && (hash === route || hash.indexOf(route + '/') === 0);
49324
+ a.classList.toggle('active', on);
49319
49325
  });
49320
49326
  }
49321
49327
 
@@ -55596,6 +55602,202 @@ async function setCloudSetting(db, key, value) {
55596
55602
  await runAsyncOrSync(db.adapter, `SELECT lattice_set_cloud_setting(?, ?)`, [key, value]);
55597
55603
  }
55598
55604
 
55605
+ // src/cloud/audience.ts
55606
+ var ROLE_NAME_RE = /^[A-Za-z0-9_-]{1,63}$/;
55607
+ var COL_RE = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
55608
+ function isRowAudience(audience) {
55609
+ const a6 = (audience ?? "").trim();
55610
+ return a6 === "" || a6 === "everyone" || a6 === "row-audience";
55611
+ }
55612
+ function audiencePredicate(audience, ctx) {
55613
+ if (isRowAudience(audience)) return "true";
55614
+ const clauses = audience.split("+").map((c6) => c6.trim()).filter(Boolean);
55615
+ const parts = [];
55616
+ for (const clause of clauses) {
55617
+ if (clause === "everyone" || clause === "row-audience") return "true";
55618
+ if (clause === "owner") {
55619
+ if (!ctx) throw new Error('lattice: the "owner" audience needs a row context');
55620
+ parts.push(`lattice_is_owner(${ctx.tableLit}, ${ctx.pkExpr})`);
55621
+ continue;
55622
+ }
55623
+ const idx = clause.indexOf(":");
55624
+ const kind = idx === -1 ? clause : clause.slice(0, idx);
55625
+ const arg = idx === -1 ? "" : clause.slice(idx + 1).trim();
55626
+ if (kind === "role") {
55627
+ if (!ROLE_NAME_RE.test(arg)) throw new Error(`lattice: invalid role in audience "${clause}"`);
55628
+ parts.push(`lattice_has_role('${arg}')`);
55629
+ } else if (kind === "subject") {
55630
+ if (!COL_RE.test(arg))
55631
+ throw new Error(`lattice: invalid subject column in audience "${clause}"`);
55632
+ parts.push(`lattice_is_subject("${arg}")`);
55633
+ } else if (kind === "source") {
55634
+ if (!COL_RE.test(arg))
55635
+ throw new Error(`lattice: invalid source column in audience "${clause}"`);
55636
+ parts.push(`lattice_source_visible("${arg}")`);
55637
+ } else {
55638
+ throw new Error(`lattice: unknown audience clause "${clause}"`);
55639
+ }
55640
+ }
55641
+ return parts.length > 0 ? parts.map((p3) => `(${p3})`).join(" OR ") : "true";
55642
+ }
55643
+ function tableNeedsAudienceView(columnAudience) {
55644
+ return Object.values(columnAudience).some((a6) => !isRowAudience(a6));
55645
+ }
55646
+ function quoteIdent(s2) {
55647
+ return `"${s2.replace(/"/g, '""')}"`;
55648
+ }
55649
+ function audienceViewSql(table, columns, pkCols, columnAudience) {
55650
+ const view = quoteIdent(`${table}_v`);
55651
+ const base = quoteIdent(table);
55652
+ const lit = `'${table.replace(/'/g, "''")}'`;
55653
+ const pkExpr = pkSqlExpr(pkCols, "");
55654
+ const selectCols = columns.map((col) => {
55655
+ const aud = columnAudience[col] ?? "";
55656
+ if (isRowAudience(aud)) return quoteIdent(col);
55657
+ const pred = audiencePredicate(aud, { tableLit: lit, pkExpr });
55658
+ if (pred === "true") return quoteIdent(col);
55659
+ const colLit = `'${col.replace(/'/g, "''")}'`;
55660
+ const full = `(${pred}) OR lattice_cell_visible(${lit}, ${pkExpr}, ${colLit})`;
55661
+ return `CASE WHEN ${full} THEN ${quoteIdent(col)} END AS ${quoteIdent(col)}`;
55662
+ });
55663
+ return [
55664
+ `CREATE OR REPLACE VIEW ${view} AS SELECT ${selectCols.join(", ")} FROM ${base} WHERE lattice_row_visible(${lit}, ${pkSqlExpr(pkCols, "")});`,
55665
+ `GRANT SELECT ON ${view} TO ${MEMBER_GROUP};`,
55666
+ `REVOKE SELECT ON ${base} FROM ${MEMBER_GROUP};`
55667
+ ].join("\n");
55668
+ }
55669
+ async function loadColumnPolicy(db, table) {
55670
+ if (db.getDialect() !== "postgres") return {};
55671
+ const rows = await allAsyncOrSync(
55672
+ db.adapter,
55673
+ `SELECT "column_name", "audience" FROM "__lattice_column_policy" WHERE "table_name" = ?`,
55674
+ [table]
55675
+ );
55676
+ const out = {};
55677
+ for (const r6 of rows) out[r6.column_name] = r6.audience;
55678
+ return out;
55679
+ }
55680
+ async function seedColumnPolicyFromYaml(db, table, yamlAudience) {
55681
+ if (db.getDialect() !== "postgres") return;
55682
+ const marker = `internal:cloud-column-seed:${table}:v1`;
55683
+ const already = await getAsyncOrSync(
55684
+ db.adapter,
55685
+ `SELECT 1 AS one FROM "__lattice_migrations" WHERE "version" = ?`,
55686
+ [marker]
55687
+ );
55688
+ if (already) return;
55689
+ for (const [col, aud] of Object.entries(yamlAudience)) {
55690
+ if (isRowAudience(aud)) continue;
55691
+ await runAsyncOrSync(
55692
+ db.adapter,
55693
+ `INSERT INTO "__lattice_column_policy" ("table_name","column_name","audience")
55694
+ VALUES (?, ?, ?) ON CONFLICT ("table_name","column_name") DO NOTHING`,
55695
+ [table, col, aud]
55696
+ );
55697
+ }
55698
+ await runAsyncOrSync(
55699
+ db.adapter,
55700
+ `INSERT INTO "__lattice_migrations" ("version","applied_at") VALUES (?, ?)
55701
+ ON CONFLICT ("version") DO NOTHING`,
55702
+ [marker, (/* @__PURE__ */ new Date()).toISOString()]
55703
+ );
55704
+ }
55705
+ async function regenerateAudienceViewFromDb(db, table, columns, pkCols) {
55706
+ if (db.getDialect() !== "postgres") return;
55707
+ if (pkCols.length === 0) return;
55708
+ const spec = await loadColumnPolicy(db, table);
55709
+ const view = quoteIdent(`${table}_v`);
55710
+ const base = quoteIdent(table);
55711
+ if (!tableNeedsAudienceView(spec)) {
55712
+ await runAsyncOrSync(
55713
+ db.adapter,
55714
+ `DROP VIEW IF EXISTS ${view};
55715
+ GRANT SELECT ON ${base} TO ${MEMBER_GROUP};`
55716
+ );
55717
+ return;
55718
+ }
55719
+ await runAsyncOrSync(db.adapter, audienceViewSql(table, columns, pkCols, spec));
55720
+ }
55721
+ async function setColumnAudience(db, table, column, audience, columns, pkCols) {
55722
+ if (db.getDialect() !== "postgres") return;
55723
+ await runAsyncOrSync(db.adapter, `SELECT lattice_set_column_audience(?, ?, ?)`, [
55724
+ table,
55725
+ column,
55726
+ audience
55727
+ ]);
55728
+ await regenerateAudienceViewFromDb(db, table, columns, pkCols);
55729
+ }
55730
+
55731
+ // src/cloud/setup.ts
55732
+ var PRIVATE_ONLY_TABLES = [...NATIVE_INTERNAL_NAMES, "secrets"];
55733
+ async function reconcileCloudMemberAccess(db) {
55734
+ if (db.getDialect() !== "postgres") return;
55735
+ const registered = db.getRegisteredTableNames();
55736
+ for (const t8 of PRIVATE_ONLY_TABLES) {
55737
+ if (!registered.includes(t8)) continue;
55738
+ await runAsyncOrSync(
55739
+ db.adapter,
55740
+ `SELECT lattice_set_table_never_share('${t8.replace(/'/g, "''")}', true)`
55741
+ );
55742
+ }
55743
+ const rlsRows = await allAsyncOrSync(
55744
+ db.adapter,
55745
+ `SELECT c.relname AS name FROM pg_class c
55746
+ JOIN pg_namespace n ON n.oid = c.relnamespace
55747
+ WHERE n.nspname = current_schema() AND c.relkind = 'r' AND c.relrowsecurity`
55748
+ );
55749
+ const rlsOn = new Set(rlsRows.map((r6) => r6.name));
55750
+ for (const table of registered) {
55751
+ if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) continue;
55752
+ if (!rlsOn.has(table)) continue;
55753
+ if (db.getPrimaryKey(table).length === 0) continue;
55754
+ const q3 = `"${table.replace(/"/g, '""')}"`;
55755
+ const masked = tableNeedsAudienceView(db.getColumnAudience(table) ?? {});
55756
+ if (masked) {
55757
+ const v2 = `"${`${table}_v`.replace(/"/g, '""')}"`;
55758
+ await runAsyncOrSync(db.adapter, `GRANT SELECT ON ${v2} TO ${MEMBER_GROUP}`);
55759
+ await runAsyncOrSync(db.adapter, `GRANT INSERT, UPDATE, DELETE ON ${q3} TO ${MEMBER_GROUP}`);
55760
+ } else {
55761
+ await runAsyncOrSync(
55762
+ db.adapter,
55763
+ `GRANT SELECT, INSERT, UPDATE, DELETE ON ${q3} TO ${MEMBER_GROUP}`
55764
+ );
55765
+ }
55766
+ }
55767
+ }
55768
+ async function secureNewCloudTable(db, table, pk) {
55769
+ if (db.getDialect() !== "postgres") return;
55770
+ if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) return;
55771
+ if (pk.length === 0) return;
55772
+ await backfillOwnership(db, table, pk);
55773
+ await enableRlsForTable(db, table, pk);
55774
+ const cols = db.getRegisteredColumns(table);
55775
+ if (cols) {
55776
+ await seedColumnPolicyFromYaml(db, table, db.getColumnAudience(table));
55777
+ await regenerateAudienceViewFromDb(db, table, Object.keys(cols), pk);
55778
+ }
55779
+ }
55780
+ async function secureCloud(db) {
55781
+ if (db.getDialect() !== "postgres") return;
55782
+ await installCloudRls(db);
55783
+ await installCloudSettings(db);
55784
+ await db.ensureObservationSubstrate();
55785
+ await enableChangelogRls(db);
55786
+ const registered = db.getRegisteredTableNames();
55787
+ for (const table of registered) {
55788
+ await secureNewCloudTable(db, table, db.getPrimaryKey(table));
55789
+ }
55790
+ await reconcileCloudMemberAccess(db);
55791
+ await runAsyncOrSync(
55792
+ db.adapter,
55793
+ `DO $LATTICE$ BEGIN
55794
+ IF to_regclass('__lattice_user_identity') IS NOT NULL THEN
55795
+ EXECUTE 'GRANT SELECT, INSERT, UPDATE ON "__lattice_user_identity" TO ${MEMBER_GROUP}';
55796
+ END IF;
55797
+ END $LATTICE$`
55798
+ );
55799
+ }
55800
+
55599
55801
  // src/cloud/members.ts
55600
55802
  import { randomBytes as randomBytes6 } from "crypto";
55601
55803
  var ROLE_RE = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
@@ -55792,134 +55994,6 @@ var FeedBus = class {
55792
55994
 
55793
55995
  // src/gui/mutations.ts
55794
55996
  import { createHash as createHash3 } from "crypto";
55795
-
55796
- // src/cloud/audience.ts
55797
- var ROLE_NAME_RE = /^[A-Za-z0-9_-]{1,63}$/;
55798
- var COL_RE = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
55799
- function isRowAudience(audience) {
55800
- const a6 = (audience ?? "").trim();
55801
- return a6 === "" || a6 === "everyone" || a6 === "row-audience";
55802
- }
55803
- function audiencePredicate(audience, ctx) {
55804
- if (isRowAudience(audience)) return "true";
55805
- const clauses = audience.split("+").map((c6) => c6.trim()).filter(Boolean);
55806
- const parts = [];
55807
- for (const clause of clauses) {
55808
- if (clause === "everyone" || clause === "row-audience") return "true";
55809
- if (clause === "owner") {
55810
- if (!ctx) throw new Error('lattice: the "owner" audience needs a row context');
55811
- parts.push(`lattice_is_owner(${ctx.tableLit}, ${ctx.pkExpr})`);
55812
- continue;
55813
- }
55814
- const idx = clause.indexOf(":");
55815
- const kind = idx === -1 ? clause : clause.slice(0, idx);
55816
- const arg = idx === -1 ? "" : clause.slice(idx + 1).trim();
55817
- if (kind === "role") {
55818
- if (!ROLE_NAME_RE.test(arg)) throw new Error(`lattice: invalid role in audience "${clause}"`);
55819
- parts.push(`lattice_has_role('${arg}')`);
55820
- } else if (kind === "subject") {
55821
- if (!COL_RE.test(arg))
55822
- throw new Error(`lattice: invalid subject column in audience "${clause}"`);
55823
- parts.push(`lattice_is_subject("${arg}")`);
55824
- } else if (kind === "source") {
55825
- if (!COL_RE.test(arg))
55826
- throw new Error(`lattice: invalid source column in audience "${clause}"`);
55827
- parts.push(`lattice_source_visible("${arg}")`);
55828
- } else {
55829
- throw new Error(`lattice: unknown audience clause "${clause}"`);
55830
- }
55831
- }
55832
- return parts.length > 0 ? parts.map((p3) => `(${p3})`).join(" OR ") : "true";
55833
- }
55834
- function tableNeedsAudienceView(columnAudience) {
55835
- return Object.values(columnAudience).some((a6) => !isRowAudience(a6));
55836
- }
55837
- function quoteIdent(s2) {
55838
- return `"${s2.replace(/"/g, '""')}"`;
55839
- }
55840
- function audienceViewSql(table, columns, pkCols, columnAudience) {
55841
- const view = quoteIdent(`${table}_v`);
55842
- const base = quoteIdent(table);
55843
- const lit = `'${table.replace(/'/g, "''")}'`;
55844
- const pkExpr = pkSqlExpr(pkCols, "");
55845
- const selectCols = columns.map((col) => {
55846
- const aud = columnAudience[col] ?? "";
55847
- if (isRowAudience(aud)) return quoteIdent(col);
55848
- const pred = audiencePredicate(aud, { tableLit: lit, pkExpr });
55849
- if (pred === "true") return quoteIdent(col);
55850
- const colLit = `'${col.replace(/'/g, "''")}'`;
55851
- const full = `(${pred}) OR lattice_cell_visible(${lit}, ${pkExpr}, ${colLit})`;
55852
- return `CASE WHEN ${full} THEN ${quoteIdent(col)} END AS ${quoteIdent(col)}`;
55853
- });
55854
- return [
55855
- `CREATE OR REPLACE VIEW ${view} AS SELECT ${selectCols.join(", ")} FROM ${base} WHERE lattice_row_visible(${lit}, ${pkSqlExpr(pkCols, "")});`,
55856
- `GRANT SELECT ON ${view} TO ${MEMBER_GROUP};`,
55857
- `REVOKE SELECT ON ${base} FROM ${MEMBER_GROUP};`
55858
- ].join("\n");
55859
- }
55860
- async function loadColumnPolicy(db, table) {
55861
- if (db.getDialect() !== "postgres") return {};
55862
- const rows = await allAsyncOrSync(
55863
- db.adapter,
55864
- `SELECT "column_name", "audience" FROM "__lattice_column_policy" WHERE "table_name" = ?`,
55865
- [table]
55866
- );
55867
- const out = {};
55868
- for (const r6 of rows) out[r6.column_name] = r6.audience;
55869
- return out;
55870
- }
55871
- async function seedColumnPolicyFromYaml(db, table, yamlAudience) {
55872
- if (db.getDialect() !== "postgres") return;
55873
- const marker = `internal:cloud-column-seed:${table}:v1`;
55874
- const already = await getAsyncOrSync(
55875
- db.adapter,
55876
- `SELECT 1 AS one FROM "__lattice_migrations" WHERE "version" = ?`,
55877
- [marker]
55878
- );
55879
- if (already) return;
55880
- for (const [col, aud] of Object.entries(yamlAudience)) {
55881
- if (isRowAudience(aud)) continue;
55882
- await runAsyncOrSync(
55883
- db.adapter,
55884
- `INSERT INTO "__lattice_column_policy" ("table_name","column_name","audience")
55885
- VALUES (?, ?, ?) ON CONFLICT ("table_name","column_name") DO NOTHING`,
55886
- [table, col, aud]
55887
- );
55888
- }
55889
- await runAsyncOrSync(
55890
- db.adapter,
55891
- `INSERT INTO "__lattice_migrations" ("version","applied_at") VALUES (?, ?)
55892
- ON CONFLICT ("version") DO NOTHING`,
55893
- [marker, (/* @__PURE__ */ new Date()).toISOString()]
55894
- );
55895
- }
55896
- async function regenerateAudienceViewFromDb(db, table, columns, pkCols) {
55897
- if (db.getDialect() !== "postgres") return;
55898
- if (pkCols.length === 0) return;
55899
- const spec = await loadColumnPolicy(db, table);
55900
- const view = quoteIdent(`${table}_v`);
55901
- const base = quoteIdent(table);
55902
- if (!tableNeedsAudienceView(spec)) {
55903
- await runAsyncOrSync(
55904
- db.adapter,
55905
- `DROP VIEW IF EXISTS ${view};
55906
- GRANT SELECT ON ${base} TO ${MEMBER_GROUP};`
55907
- );
55908
- return;
55909
- }
55910
- await runAsyncOrSync(db.adapter, audienceViewSql(table, columns, pkCols, spec));
55911
- }
55912
- async function setColumnAudience(db, table, column, audience, columns, pkCols) {
55913
- if (db.getDialect() !== "postgres") return;
55914
- await runAsyncOrSync(db.adapter, `SELECT lattice_set_column_audience(?, ?, ?)`, [
55915
- table,
55916
- column,
55917
- audience
55918
- ]);
55919
- await regenerateAudienceViewFromDb(db, table, columns, pkCols);
55920
- }
55921
-
55922
- // src/gui/mutations.ts
55923
55997
  function rowLabel2(row) {
55924
55998
  if (!row || typeof row !== "object") return null;
55925
55999
  const r6 = row;
@@ -56346,42 +56420,6 @@ function saveConfigDoc(configPath, doc) {
56346
56420
  writeFileSync6(configPath, doc.toString(), "utf8");
56347
56421
  }
56348
56422
 
56349
- // src/cloud/setup.ts
56350
- async function secureNewCloudTable(db, table, pk) {
56351
- if (db.getDialect() !== "postgres") return;
56352
- if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) return;
56353
- if (pk.length === 0) return;
56354
- await backfillOwnership(db, table, pk);
56355
- await enableRlsForTable(db, table, pk);
56356
- const cols = db.getRegisteredColumns(table);
56357
- if (cols) {
56358
- await seedColumnPolicyFromYaml(db, table, db.getColumnAudience(table));
56359
- await regenerateAudienceViewFromDb(db, table, Object.keys(cols), pk);
56360
- }
56361
- }
56362
- async function secureCloud(db) {
56363
- if (db.getDialect() !== "postgres") return;
56364
- await installCloudRls(db);
56365
- await installCloudSettings(db);
56366
- await db.ensureObservationSubstrate();
56367
- await enableChangelogRls(db);
56368
- const registered = db.getRegisteredTableNames();
56369
- for (const table of registered) {
56370
- await secureNewCloudTable(db, table, db.getPrimaryKey(table));
56371
- }
56372
- if (registered.includes("secrets")) {
56373
- await runAsyncOrSync(db.adapter, `SELECT lattice_set_table_never_share('secrets', true)`);
56374
- }
56375
- await runAsyncOrSync(
56376
- db.adapter,
56377
- `DO $LATTICE$ BEGIN
56378
- IF to_regclass('__lattice_user_identity') IS NOT NULL THEN
56379
- EXECUTE 'GRANT SELECT, INSERT, UPDATE ON "__lattice_user_identity" TO ${MEMBER_GROUP}';
56380
- END IF;
56381
- END $LATTICE$`
56382
- );
56383
- }
56384
-
56385
56423
  // src/gui/schema-ops.ts
56386
56424
  async function secureRuntimeTableIfCloud(active, name, pk) {
56387
56425
  const db = active.db;
@@ -62171,6 +62209,7 @@ async function openConfig(configPath, outputDir, autoRender = false) {
62171
62209
  const knownTables = /* @__PURE__ */ new Set([...declared, ...discovered.map((t8) => t8.name)]);
62172
62210
  for (const t8 of discovered) {
62173
62211
  if (declared.has(t8.name)) continue;
62212
+ if (t8.columns.length === 0) continue;
62174
62213
  db.define(t8.name, {
62175
62214
  columns: Object.fromEntries(t8.columns.map((c6) => [c6, "TEXT"])),
62176
62215
  ...t8.pk.length > 0 ? { primaryKey: t8.pk.length === 1 ? t8.pk[0] : t8.pk } : {},
@@ -62207,6 +62246,7 @@ async function openConfig(configPath, outputDir, autoRender = false) {
62207
62246
  await installCloudSettings(db);
62208
62247
  await db.ensureObservationSubstrate();
62209
62248
  await enableChangelogRls(db);
62249
+ await reconcileCloudMemberAccess(db);
62210
62250
  }
62211
62251
  } catch (e6) {
62212
62252
  console.error("[openConfig] cloud bootstrap converge failed:", e6.message);
package/dist/index.cjs CHANGED
@@ -46770,6 +46770,10 @@ var NATIVE_ENTITY_NAMES = new Set(Object.keys(NATIVE_ENTITY_DEFS));
46770
46770
  function isNativeEntity(name) {
46771
46771
  return NATIVE_ENTITY_NAMES.has(name);
46772
46772
  }
46773
+ var NATIVE_INTERNAL_NAMES = /* @__PURE__ */ new Set([
46774
+ "chat_threads",
46775
+ "chat_messages"
46776
+ ]);
46773
46777
  function registerNativeEntities(db) {
46774
46778
  const existing = new Set(db.getRegisteredTableNames());
46775
46779
  for (const [name, def] of Object.entries(NATIVE_ENTITY_DEFS)) {
@@ -48469,6 +48473,42 @@ async function setCloudSetting(db, key, value) {
48469
48473
  }
48470
48474
 
48471
48475
  // src/cloud/setup.ts
48476
+ var PRIVATE_ONLY_TABLES = [...NATIVE_INTERNAL_NAMES, "secrets"];
48477
+ async function reconcileCloudMemberAccess(db) {
48478
+ if (db.getDialect() !== "postgres") return;
48479
+ const registered = db.getRegisteredTableNames();
48480
+ for (const t8 of PRIVATE_ONLY_TABLES) {
48481
+ if (!registered.includes(t8)) continue;
48482
+ await runAsyncOrSync(
48483
+ db.adapter,
48484
+ `SELECT lattice_set_table_never_share('${t8.replace(/'/g, "''")}', true)`
48485
+ );
48486
+ }
48487
+ const rlsRows = await allAsyncOrSync(
48488
+ db.adapter,
48489
+ `SELECT c.relname AS name FROM pg_class c
48490
+ JOIN pg_namespace n ON n.oid = c.relnamespace
48491
+ WHERE n.nspname = current_schema() AND c.relkind = 'r' AND c.relrowsecurity`
48492
+ );
48493
+ const rlsOn = new Set(rlsRows.map((r6) => r6.name));
48494
+ for (const table of registered) {
48495
+ if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) continue;
48496
+ if (!rlsOn.has(table)) continue;
48497
+ if (db.getPrimaryKey(table).length === 0) continue;
48498
+ const q3 = `"${table.replace(/"/g, '""')}"`;
48499
+ const masked = tableNeedsAudienceView(db.getColumnAudience(table) ?? {});
48500
+ if (masked) {
48501
+ const v2 = `"${`${table}_v`.replace(/"/g, '""')}"`;
48502
+ await runAsyncOrSync(db.adapter, `GRANT SELECT ON ${v2} TO ${MEMBER_GROUP}`);
48503
+ await runAsyncOrSync(db.adapter, `GRANT INSERT, UPDATE, DELETE ON ${q3} TO ${MEMBER_GROUP}`);
48504
+ } else {
48505
+ await runAsyncOrSync(
48506
+ db.adapter,
48507
+ `GRANT SELECT, INSERT, UPDATE, DELETE ON ${q3} TO ${MEMBER_GROUP}`
48508
+ );
48509
+ }
48510
+ }
48511
+ }
48472
48512
  async function secureNewCloudTable(db, table, pk) {
48473
48513
  if (db.getDialect() !== "postgres") return;
48474
48514
  if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) return;
@@ -48491,9 +48531,7 @@ async function secureCloud(db) {
48491
48531
  for (const table of registered) {
48492
48532
  await secureNewCloudTable(db, table, db.getPrimaryKey(table));
48493
48533
  }
48494
- if (registered.includes("secrets")) {
48495
- await runAsyncOrSync(db.adapter, `SELECT lattice_set_table_never_share('secrets', true)`);
48496
- }
48534
+ await reconcileCloudMemberAccess(db);
48497
48535
  await runAsyncOrSync(
48498
48536
  db.adapter,
48499
48537
  `DO $LATTICE$ BEGIN
package/dist/index.js CHANGED
@@ -46592,6 +46592,10 @@ var NATIVE_ENTITY_NAMES = new Set(Object.keys(NATIVE_ENTITY_DEFS));
46592
46592
  function isNativeEntity(name) {
46593
46593
  return NATIVE_ENTITY_NAMES.has(name);
46594
46594
  }
46595
+ var NATIVE_INTERNAL_NAMES = /* @__PURE__ */ new Set([
46596
+ "chat_threads",
46597
+ "chat_messages"
46598
+ ]);
46595
46599
  function registerNativeEntities(db) {
46596
46600
  const existing = new Set(db.getRegisteredTableNames());
46597
46601
  for (const [name, def] of Object.entries(NATIVE_ENTITY_DEFS)) {
@@ -48291,6 +48295,42 @@ async function setCloudSetting(db, key, value) {
48291
48295
  }
48292
48296
 
48293
48297
  // src/cloud/setup.ts
48298
+ var PRIVATE_ONLY_TABLES = [...NATIVE_INTERNAL_NAMES, "secrets"];
48299
+ async function reconcileCloudMemberAccess(db) {
48300
+ if (db.getDialect() !== "postgres") return;
48301
+ const registered = db.getRegisteredTableNames();
48302
+ for (const t8 of PRIVATE_ONLY_TABLES) {
48303
+ if (!registered.includes(t8)) continue;
48304
+ await runAsyncOrSync(
48305
+ db.adapter,
48306
+ `SELECT lattice_set_table_never_share('${t8.replace(/'/g, "''")}', true)`
48307
+ );
48308
+ }
48309
+ const rlsRows = await allAsyncOrSync(
48310
+ db.adapter,
48311
+ `SELECT c.relname AS name FROM pg_class c
48312
+ JOIN pg_namespace n ON n.oid = c.relnamespace
48313
+ WHERE n.nspname = current_schema() AND c.relkind = 'r' AND c.relrowsecurity`
48314
+ );
48315
+ const rlsOn = new Set(rlsRows.map((r6) => r6.name));
48316
+ for (const table of registered) {
48317
+ if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) continue;
48318
+ if (!rlsOn.has(table)) continue;
48319
+ if (db.getPrimaryKey(table).length === 0) continue;
48320
+ const q3 = `"${table.replace(/"/g, '""')}"`;
48321
+ const masked = tableNeedsAudienceView(db.getColumnAudience(table) ?? {});
48322
+ if (masked) {
48323
+ const v2 = `"${`${table}_v`.replace(/"/g, '""')}"`;
48324
+ await runAsyncOrSync(db.adapter, `GRANT SELECT ON ${v2} TO ${MEMBER_GROUP}`);
48325
+ await runAsyncOrSync(db.adapter, `GRANT INSERT, UPDATE, DELETE ON ${q3} TO ${MEMBER_GROUP}`);
48326
+ } else {
48327
+ await runAsyncOrSync(
48328
+ db.adapter,
48329
+ `GRANT SELECT, INSERT, UPDATE, DELETE ON ${q3} TO ${MEMBER_GROUP}`
48330
+ );
48331
+ }
48332
+ }
48333
+ }
48294
48334
  async function secureNewCloudTable(db, table, pk) {
48295
48335
  if (db.getDialect() !== "postgres") return;
48296
48336
  if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) return;
@@ -48313,9 +48353,7 @@ async function secureCloud(db) {
48313
48353
  for (const table of registered) {
48314
48354
  await secureNewCloudTable(db, table, db.getPrimaryKey(table));
48315
48355
  }
48316
- if (registered.includes("secrets")) {
48317
- await runAsyncOrSync(db.adapter, `SELECT lattice_set_table_never_share('secrets', true)`);
48318
- }
48356
+ await reconcileCloudMemberAccess(db);
48319
48357
  await runAsyncOrSync(
48320
48358
  db.adapter,
48321
48359
  `DO $LATTICE$ BEGIN
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latticesql",
3
- "version": "3.2.0",
3
+ "version": "3.2.1",
4
4
  "description": "Persistent structured memory for AI agent systems — pluggable SQLite or Postgres backend, LLM context bridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",