latticesql 3.4.1 → 3.4.2

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
@@ -8131,6 +8131,33 @@ async function ownPolyfillsByGroup(db) {
8131
8131
  }
8132
8132
  }
8133
8133
  }
8134
+ async function enableGuiAuditRls(db) {
8135
+ if (!isPg(db)) return;
8136
+ const reg = await getAsyncOrSync(db.adapter, `SELECT to_regclass($1) AS reg`, [
8137
+ "_lattice_gui_audit"
8138
+ ]);
8139
+ if (reg?.reg == null) return;
8140
+ await runCloudBootstrapSql(
8141
+ db,
8142
+ `
8143
+ ALTER TABLE "_lattice_gui_audit" ENABLE ROW LEVEL SECURITY;
8144
+ ALTER TABLE "_lattice_gui_audit" FORCE ROW LEVEL SECURITY;
8145
+ DROP POLICY IF EXISTS "lattice_gui_audit_owner" ON "_lattice_gui_audit";
8146
+ DROP POLICY IF EXISTS "lattice_gui_audit_sel" ON "_lattice_gui_audit";
8147
+ CREATE POLICY "lattice_gui_audit_sel" ON "_lattice_gui_audit" FOR SELECT
8148
+ USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
8149
+ DROP POLICY IF EXISTS "lattice_gui_audit_ins" ON "_lattice_gui_audit";
8150
+ CREATE POLICY "lattice_gui_audit_ins" ON "_lattice_gui_audit" FOR INSERT
8151
+ WITH CHECK ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
8152
+ DROP POLICY IF EXISTS "lattice_gui_audit_upd" ON "_lattice_gui_audit";
8153
+ CREATE POLICY "lattice_gui_audit_upd" ON "_lattice_gui_audit" FOR UPDATE
8154
+ USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
8155
+ DROP POLICY IF EXISTS "lattice_gui_audit_del" ON "_lattice_gui_audit";
8156
+ CREATE POLICY "lattice_gui_audit_del" ON "_lattice_gui_audit" FOR DELETE
8157
+ USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
8158
+ `
8159
+ );
8160
+ }
8134
8161
  async function installCloudRls(db) {
8135
8162
  if (!isPg(db)) return;
8136
8163
  const schema = await cloudSchema(db);
@@ -8296,6 +8323,18 @@ CREATE TABLE IF NOT EXISTS "__lattice_member_invites" (
8296
8323
  -- bootstrap is now run directly + idempotently, not version-gated).
8297
8324
  ALTER TABLE "__lattice_member_invites" ADD COLUMN IF NOT EXISTS "email" text;
8298
8325
 
8326
+ -- Owner-published entity/render LAYOUT (the entities + entityContexts config
8327
+ -- blocks), so a joined member \u2014 whose generated config has entities: {} \u2014 can
8328
+ -- hydrate the full render layout and produce a complete context tree. This holds
8329
+ -- schema CONFIG, not row data, so it is safe to share with members (granted
8330
+ -- SELECT). A shared singleton, like __lattice_user_identity: no per-row RLS.
8331
+ CREATE TABLE IF NOT EXISTS "__lattice_shared_schema" (
8332
+ "id" TEXT PRIMARY KEY DEFAULT 'singleton',
8333
+ "entities_json" TEXT,
8334
+ "contexts_json" TEXT,
8335
+ "updated_at" TEXT
8336
+ );
8337
+
8299
8338
  -- #3.1 \u2014 one-time-use + revocation enforcement. After a member authenticates to
8300
8339
  -- the cloud with their minted credential, the join path calls this to CLAIM the
8301
8340
  -- invite. The single atomic UPDATE stamps redeemed_at and returns true ONLY when
@@ -10173,7 +10212,7 @@ var init_registry = __esm({
10173
10212
  });
10174
10213
 
10175
10214
  // src/gui/ai/lattice-docs.ts
10176
- import { readFileSync as readFileSync13, readdirSync as readdirSync5, existsSync as existsSync17 } from "fs";
10215
+ import { readFileSync as readFileSync14, readdirSync as readdirSync5, existsSync as existsSync17 } from "fs";
10177
10216
  import { dirname as dirname8, join as join16 } from "path";
10178
10217
  import { fileURLToPath as fileURLToPath2 } from "url";
10179
10218
  function findDocsDir() {
@@ -10233,7 +10272,7 @@ function allSections() {
10233
10272
  }
10234
10273
  for (const f6 of files) {
10235
10274
  try {
10236
- out.push(...sectionsOf(f6, readFileSync13(join16(dir, f6), "utf8")));
10275
+ out.push(...sectionsOf(f6, readFileSync14(join16(dir, f6), "utf8")));
10237
10276
  } catch {
10238
10277
  }
10239
10278
  }
@@ -62767,6 +62806,87 @@ async function discoverCloudTables(db) {
62767
62806
  // src/gui/server.ts
62768
62807
  init_rls();
62769
62808
 
62809
+ // src/cloud/shared-schema.ts
62810
+ init_lattice();
62811
+ init_adapter();
62812
+
62813
+ // src/gui/config-io.ts
62814
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
62815
+ import { parseDocument as parseDocument2 } from "yaml";
62816
+ async function execSql(db, sql) {
62817
+ const adapter = db._adapter;
62818
+ if (!adapter.runAsync) throw new Error("Adapter does not support runAsync");
62819
+ await adapter.runAsync(sql);
62820
+ }
62821
+ function loadConfigDoc(configPath) {
62822
+ return parseDocument2(readFileSync13(configPath, "utf8"));
62823
+ }
62824
+ function saveConfigDoc(configPath, doc) {
62825
+ writeFileSync6(configPath, doc.toString(), "utf8");
62826
+ }
62827
+
62828
+ // src/cloud/shared-schema.ts
62829
+ async function publishSharedSchema(db, configPath) {
62830
+ if (db.getDialect() !== "postgres") return;
62831
+ const cfg = loadConfigDoc(configPath).toJSON();
62832
+ const entities = cfg.entities ?? {};
62833
+ if (Object.keys(entities).length === 0) return;
62834
+ await runAsyncOrSync(
62835
+ db.adapter,
62836
+ `INSERT INTO "__lattice_shared_schema" ("id","entities_json","contexts_json","updated_at")
62837
+ VALUES ('singleton', $1, $2, $3)
62838
+ ON CONFLICT ("id") DO UPDATE SET
62839
+ "entities_json" = EXCLUDED."entities_json",
62840
+ "contexts_json" = EXCLUDED."contexts_json",
62841
+ "updated_at" = EXCLUDED."updated_at"`,
62842
+ [
62843
+ JSON.stringify(entities),
62844
+ JSON.stringify(cfg.entityContexts ?? null),
62845
+ (/* @__PURE__ */ new Date()).toISOString()
62846
+ ]
62847
+ );
62848
+ }
62849
+ async function hydrateMemberConfigFromCloud(configPath, dbUrl, encryptionKey) {
62850
+ if (!isPostgresUrl(dbUrl)) return false;
62851
+ const existing = loadConfigDoc(configPath).toJSON();
62852
+ if (Object.keys(existing.entities ?? {}).length > 0) return false;
62853
+ try {
62854
+ const peek = new Lattice({ config: configPath }, { encryptionKey });
62855
+ try {
62856
+ await peek.init({ introspectOnly: true });
62857
+ const reg = await getAsyncOrSync(
62858
+ peek.adapter,
62859
+ "SELECT to_regclass('__lattice_shared_schema') AS reg"
62860
+ );
62861
+ if (reg?.reg == null) return false;
62862
+ const row = await getAsyncOrSync(
62863
+ peek.adapter,
62864
+ 'SELECT "entities_json","contexts_json" FROM "__lattice_shared_schema" WHERE "id" = $1',
62865
+ ["singleton"]
62866
+ );
62867
+ if (row?.entities_json == null) return false;
62868
+ const entities = JSON.parse(row.entities_json);
62869
+ if (Object.keys(entities).length === 0) return false;
62870
+ const doc = loadConfigDoc(configPath);
62871
+ doc.setIn(["entities"], entities);
62872
+ if (row.contexts_json != null) {
62873
+ const ctx = JSON.parse(row.contexts_json);
62874
+ if (ctx) doc.setIn(["entityContexts"], ctx);
62875
+ }
62876
+ saveConfigDoc(configPath, doc);
62877
+ return true;
62878
+ } finally {
62879
+ peek.close();
62880
+ }
62881
+ } catch (e6) {
62882
+ console.warn(
62883
+ "[hydrateMemberConfigFromCloud] could not hydrate member schema:",
62884
+ e6.message
62885
+ );
62886
+ return false;
62887
+ }
62888
+ }
62889
+
62770
62890
  // src/cloud/settings.ts
62771
62891
  init_adapter();
62772
62892
  init_rls();
@@ -62870,7 +62990,7 @@ var MEMBER_READABLE_BOOKKEEPING = [
62870
62990
  {
62871
62991
  name: "_lattice_gui_audit",
62872
62992
  privs: "SELECT, INSERT",
62873
- why: "the member's own GUI undo/redo log (session-scoped)"
62993
+ why: "GUI undo/redo + version history; RLS (enableGuiAuditRls) scopes reads to entries whose underlying row the member can see"
62874
62994
  },
62875
62995
  {
62876
62996
  name: "__lattice_user_identity",
@@ -62881,6 +63001,11 @@ var MEMBER_READABLE_BOOKKEEPING = [
62881
63001
  name: "__lattice_changelog",
62882
63002
  privs: "SELECT, INSERT",
62883
63003
  why: "per-viewer-RLS-filtered change history for observe()/history (the policy filters reads, so the base grant is safe)"
63004
+ },
63005
+ {
63006
+ name: "__lattice_shared_schema",
63007
+ privs: "SELECT",
63008
+ why: "owner-published entity/render layout (entities + entityContexts) a joined member hydrates its config from so render produces the full context tree"
62884
63009
  }
62885
63010
  ];
62886
63011
  var MEMBER_EXECUTE_FUNCTIONS = [
@@ -63037,6 +63162,7 @@ async function secureCloud(db) {
63037
63162
  await db.ensureObservationSubstrate();
63038
63163
  await enableChangelogRls(db);
63039
63164
  await enableChatPrivacyRls(db);
63165
+ await enableGuiAuditRls(db);
63040
63166
  await convergeLegacyColumnAudience(db);
63041
63167
  const registered = db.getRegisteredTableNames();
63042
63168
  for (const table of registered) {
@@ -63133,21 +63259,6 @@ var FeedBus = class {
63133
63259
  init_fts();
63134
63260
  init_mutations();
63135
63261
 
63136
- // src/gui/config-io.ts
63137
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
63138
- import { parseDocument as parseDocument2 } from "yaml";
63139
- async function execSql(db, sql) {
63140
- const adapter = db._adapter;
63141
- if (!adapter.runAsync) throw new Error("Adapter does not support runAsync");
63142
- await adapter.runAsync(sql);
63143
- }
63144
- function loadConfigDoc(configPath) {
63145
- return parseDocument2(readFileSync14(configPath, "utf8"));
63146
- }
63147
- function saveConfigDoc(configPath, doc) {
63148
- writeFileSync6(configPath, doc.toString(), "utf8");
63149
- }
63150
-
63151
63262
  // src/gui/schema-ops.ts
63152
63263
  init_parser();
63153
63264
  init_canonical_context();
@@ -64683,6 +64794,7 @@ async function dispatchDbConfigRoute(req, res, ctx) {
64683
64794
  const result = await migrateLatticeData(ctx.db, target);
64684
64795
  await target.rebuildFtsIndexes();
64685
64796
  await secureCloud(target);
64797
+ await publishSharedSchema(target, ctx.configPath);
64686
64798
  target.close();
64687
64799
  const sourceDbPath = parseConfigFile(ctx.configPath).dbPath;
64688
64800
  const backupPath = archiveLocalSqlite(sourceDbPath);
@@ -66541,10 +66653,13 @@ function resolveOutputDirForConfig(configPath) {
66541
66653
  async function openConfig(configPath, outputDir, autoRender = false, realtimeWatchdogMs) {
66542
66654
  healRawDbUrl(configPath);
66543
66655
  const parsed = parseConfigFile(configPath);
66656
+ const encryptionKey = getOrCreateMasterKey();
66544
66657
  if (!/^postgres(ql)?:\/\//i.test(parsed.dbPath) && !parsed.dbPath.startsWith("file:") && parsed.dbPath !== ":memory:") {
66545
66658
  mkdirSync11(dirname14(parsed.dbPath), { recursive: true });
66546
66659
  }
66547
- const encryptionKey = getOrCreateMasterKey();
66660
+ if (/^postgres(ql)?:\/\//i.test(parsed.dbPath)) {
66661
+ await hydrateMemberConfigFromCloud(configPath, parsed.dbPath, encryptionKey);
66662
+ }
66548
66663
  const db = new Lattice({ config: configPath }, { encryptionKey });
66549
66664
  registerNativeEntities(db);
66550
66665
  db.define("_lattice_gui_meta", {
@@ -66701,16 +66816,27 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
66701
66816
  await db.ensureObservationSubstrate();
66702
66817
  await enableChangelogRls(db);
66703
66818
  await enableChatPrivacyRls(db);
66819
+ await enableGuiAuditRls(db);
66704
66820
  const access = await reconcileCloudMemberAccess(db);
66705
66821
  convergeWarnings = access.skipped;
66706
66822
  for (const s2 of convergeWarnings) {
66707
66823
  console.warn(`[openConfig] cloud converge could not manage "${s2.table}": ${s2.reason}`);
66708
66824
  }
66825
+ await publishSharedSchema(db, configPath);
66709
66826
  }
66710
66827
  } catch (e6) {
66711
66828
  console.error("[openConfig] cloud bootstrap converge failed:", e6.message);
66712
66829
  }
66713
66830
  }
66831
+ if (memberOpen) {
66832
+ const userTables = db.getRegisteredTableNames().filter((t8) => !t8.startsWith("_") && !discoveredJunctions.has(t8));
66833
+ if (userTables.length === 0) {
66834
+ convergeWarnings.push({
66835
+ table: "(schema)",
66836
+ reason: "No entity layout is configured for this cloud workspace yet \u2014 ask the cloud owner to open the workspace once so it publishes the schema, then reopen. Until then, render produces no context files."
66837
+ });
66838
+ }
66839
+ }
66714
66840
  const validTables = new Set(parsed.tables.map((t8) => t8.name));
66715
66841
  for (const name of db.getRegisteredTableNames()) {
66716
66842
  if (name.startsWith("__lattice_") || name.startsWith("_lattice_")) continue;
@@ -69298,7 +69424,7 @@ function printHelp() {
69298
69424
  );
69299
69425
  }
69300
69426
  function getVersion() {
69301
- if (true) return "3.4.1";
69427
+ if (true) return "3.4.2";
69302
69428
  try {
69303
69429
  const pkgPath = new URL("../package.json", import.meta.url).pathname;
69304
69430
  const pkg = JSON.parse(readFileSync20(pkgPath, "utf-8"));
@@ -69366,14 +69492,17 @@ function runGenerate(args) {
69366
69492
  }
69367
69493
  async function runRender(args) {
69368
69494
  const outputDir = resolve11(args.output);
69495
+ const configPath = resolve11(args.config);
69369
69496
  let parsed;
69370
69497
  try {
69371
- parsed = parseConfigFile(resolve11(args.config));
69498
+ parsed = parseConfigFile(configPath);
69372
69499
  } catch (e6) {
69373
69500
  console.error(`Error: ${e6.message}`);
69374
69501
  process.exit(1);
69375
69502
  }
69376
- const db = new Lattice({ config: resolve11(args.config) });
69503
+ const encryptionKey = getOrCreateMasterKey();
69504
+ await hydrateMemberConfigFromCloud(configPath, parsed.dbPath, encryptionKey);
69505
+ const db = new Lattice({ config: configPath }, { encryptionKey });
69377
69506
  try {
69378
69507
  await db.init();
69379
69508
  const start = Date.now();
@@ -69389,7 +69518,6 @@ async function runRender(args) {
69389
69518
  } finally {
69390
69519
  db.close();
69391
69520
  }
69392
- void parsed;
69393
69521
  }
69394
69522
  async function runReconcile(args, isDryRun) {
69395
69523
  const outputDir = resolve11(args.output);
package/dist/index.cjs CHANGED
@@ -47378,6 +47378,33 @@ async function ownPolyfillsByGroup(db) {
47378
47378
  }
47379
47379
  }
47380
47380
  }
47381
+ async function enableGuiAuditRls(db) {
47382
+ if (!isPg(db)) return;
47383
+ const reg = await getAsyncOrSync(db.adapter, `SELECT to_regclass($1) AS reg`, [
47384
+ "_lattice_gui_audit"
47385
+ ]);
47386
+ if (reg?.reg == null) return;
47387
+ await runCloudBootstrapSql(
47388
+ db,
47389
+ `
47390
+ ALTER TABLE "_lattice_gui_audit" ENABLE ROW LEVEL SECURITY;
47391
+ ALTER TABLE "_lattice_gui_audit" FORCE ROW LEVEL SECURITY;
47392
+ DROP POLICY IF EXISTS "lattice_gui_audit_owner" ON "_lattice_gui_audit";
47393
+ DROP POLICY IF EXISTS "lattice_gui_audit_sel" ON "_lattice_gui_audit";
47394
+ CREATE POLICY "lattice_gui_audit_sel" ON "_lattice_gui_audit" FOR SELECT
47395
+ USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
47396
+ DROP POLICY IF EXISTS "lattice_gui_audit_ins" ON "_lattice_gui_audit";
47397
+ CREATE POLICY "lattice_gui_audit_ins" ON "_lattice_gui_audit" FOR INSERT
47398
+ WITH CHECK ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
47399
+ DROP POLICY IF EXISTS "lattice_gui_audit_upd" ON "_lattice_gui_audit";
47400
+ CREATE POLICY "lattice_gui_audit_upd" ON "_lattice_gui_audit" FOR UPDATE
47401
+ USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
47402
+ DROP POLICY IF EXISTS "lattice_gui_audit_del" ON "_lattice_gui_audit";
47403
+ CREATE POLICY "lattice_gui_audit_del" ON "_lattice_gui_audit" FOR DELETE
47404
+ USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
47405
+ `
47406
+ );
47407
+ }
47381
47408
  async function installCloudRls(db) {
47382
47409
  if (!isPg(db)) return;
47383
47410
  const schema = await cloudSchema(db);
@@ -47543,6 +47570,18 @@ CREATE TABLE IF NOT EXISTS "__lattice_member_invites" (
47543
47570
  -- bootstrap is now run directly + idempotently, not version-gated).
47544
47571
  ALTER TABLE "__lattice_member_invites" ADD COLUMN IF NOT EXISTS "email" text;
47545
47572
 
47573
+ -- Owner-published entity/render LAYOUT (the entities + entityContexts config
47574
+ -- blocks), so a joined member \u2014 whose generated config has entities: {} \u2014 can
47575
+ -- hydrate the full render layout and produce a complete context tree. This holds
47576
+ -- schema CONFIG, not row data, so it is safe to share with members (granted
47577
+ -- SELECT). A shared singleton, like __lattice_user_identity: no per-row RLS.
47578
+ CREATE TABLE IF NOT EXISTS "__lattice_shared_schema" (
47579
+ "id" TEXT PRIMARY KEY DEFAULT 'singleton',
47580
+ "entities_json" TEXT,
47581
+ "contexts_json" TEXT,
47582
+ "updated_at" TEXT
47583
+ );
47584
+
47546
47585
  -- #3.1 \u2014 one-time-use + revocation enforcement. After a member authenticates to
47547
47586
  -- the cloud with their minted credential, the join path calls this to CLAIM the
47548
47587
  -- invite. The single atomic UPDATE stamps redeemed_at and returns true ONLY when
@@ -50471,7 +50510,7 @@ function findDocsDir() {
50471
50510
  }
50472
50511
  for (let i6 = 0; i6 < 8; i6++) {
50473
50512
  const candidate = (0, import_node_path31.join)(dir, "docs");
50474
- if ((0, import_node_fs28.existsSync)((0, import_node_path31.join)(candidate, "cloud.md"))) {
50513
+ if ((0, import_node_fs29.existsSync)((0, import_node_path31.join)(candidate, "cloud.md"))) {
50475
50514
  _docsDir = candidate;
50476
50515
  return _docsDir;
50477
50516
  }
@@ -50512,13 +50551,13 @@ function allSections() {
50512
50551
  const out = [];
50513
50552
  let files = [];
50514
50553
  try {
50515
- files = (0, import_node_fs28.readdirSync)(dir).filter((f6) => f6.endsWith(".md"));
50554
+ files = (0, import_node_fs29.readdirSync)(dir).filter((f6) => f6.endsWith(".md"));
50516
50555
  } catch {
50517
50556
  files = [];
50518
50557
  }
50519
50558
  for (const f6 of files) {
50520
50559
  try {
50521
- out.push(...sectionsOf(f6, (0, import_node_fs28.readFileSync)((0, import_node_path31.join)(dir, f6), "utf8")));
50560
+ out.push(...sectionsOf(f6, (0, import_node_fs29.readFileSync)((0, import_node_path31.join)(dir, f6), "utf8")));
50522
50561
  } catch {
50523
50562
  }
50524
50563
  }
@@ -50560,11 +50599,11 @@ function searchLatticeDocs(query, limit = 4) {
50560
50599
  }))
50561
50600
  };
50562
50601
  }
50563
- var import_node_fs28, import_node_path31, import_node_url2, import_meta5, _docsDir, MAX_SECTION_CHARS, _cache;
50602
+ var import_node_fs29, import_node_path31, import_node_url2, import_meta5, _docsDir, MAX_SECTION_CHARS, _cache;
50564
50603
  var init_lattice_docs = __esm({
50565
50604
  "src/gui/ai/lattice-docs.ts"() {
50566
50605
  "use strict";
50567
- import_node_fs28 = require("fs");
50606
+ import_node_fs29 = require("fs");
50568
50607
  import_node_path31 = require("path");
50569
50608
  import_node_url2 = require("url");
50570
50609
  import_meta5 = {};
@@ -54632,7 +54671,7 @@ var MEMBER_READABLE_BOOKKEEPING = [
54632
54671
  {
54633
54672
  name: "_lattice_gui_audit",
54634
54673
  privs: "SELECT, INSERT",
54635
- why: "the member's own GUI undo/redo log (session-scoped)"
54674
+ why: "GUI undo/redo + version history; RLS (enableGuiAuditRls) scopes reads to entries whose underlying row the member can see"
54636
54675
  },
54637
54676
  {
54638
54677
  name: "__lattice_user_identity",
@@ -54643,6 +54682,11 @@ var MEMBER_READABLE_BOOKKEEPING = [
54643
54682
  name: "__lattice_changelog",
54644
54683
  privs: "SELECT, INSERT",
54645
54684
  why: "per-viewer-RLS-filtered change history for observe()/history (the policy filters reads, so the base grant is safe)"
54685
+ },
54686
+ {
54687
+ name: "__lattice_shared_schema",
54688
+ privs: "SELECT",
54689
+ why: "owner-published entity/render layout (entities + entityContexts) a joined member hydrates its config from so render produces the full context tree"
54646
54690
  }
54647
54691
  ];
54648
54692
  var MEMBER_EXECUTE_FUNCTIONS = [
@@ -54799,6 +54843,7 @@ async function secureCloud(db) {
54799
54843
  await db.ensureObservationSubstrate();
54800
54844
  await enableChangelogRls(db);
54801
54845
  await enableChatPrivacyRls(db);
54846
+ await enableGuiAuditRls(db);
54802
54847
  await convergeLegacyColumnAudience(db);
54803
54848
  const registered = db.getRegisteredTableNames();
54804
54849
  for (const table of registered) {
@@ -64752,6 +64797,87 @@ init_postgres();
64752
64797
  init_cloud_connect();
64753
64798
  init_rls();
64754
64799
 
64800
+ // src/cloud/shared-schema.ts
64801
+ init_lattice();
64802
+ init_adapter();
64803
+
64804
+ // src/gui/config-io.ts
64805
+ var import_node_fs28 = require("fs");
64806
+ var import_yaml4 = require("yaml");
64807
+ async function execSql(db, sql) {
64808
+ const adapter = db._adapter;
64809
+ if (!adapter.runAsync) throw new Error("Adapter does not support runAsync");
64810
+ await adapter.runAsync(sql);
64811
+ }
64812
+ function loadConfigDoc(configPath) {
64813
+ return (0, import_yaml4.parseDocument)((0, import_node_fs28.readFileSync)(configPath, "utf8"));
64814
+ }
64815
+ function saveConfigDoc(configPath, doc) {
64816
+ (0, import_node_fs28.writeFileSync)(configPath, doc.toString(), "utf8");
64817
+ }
64818
+
64819
+ // src/cloud/shared-schema.ts
64820
+ async function publishSharedSchema(db, configPath) {
64821
+ if (db.getDialect() !== "postgres") return;
64822
+ const cfg = loadConfigDoc(configPath).toJSON();
64823
+ const entities = cfg.entities ?? {};
64824
+ if (Object.keys(entities).length === 0) return;
64825
+ await runAsyncOrSync(
64826
+ db.adapter,
64827
+ `INSERT INTO "__lattice_shared_schema" ("id","entities_json","contexts_json","updated_at")
64828
+ VALUES ('singleton', $1, $2, $3)
64829
+ ON CONFLICT ("id") DO UPDATE SET
64830
+ "entities_json" = EXCLUDED."entities_json",
64831
+ "contexts_json" = EXCLUDED."contexts_json",
64832
+ "updated_at" = EXCLUDED."updated_at"`,
64833
+ [
64834
+ JSON.stringify(entities),
64835
+ JSON.stringify(cfg.entityContexts ?? null),
64836
+ (/* @__PURE__ */ new Date()).toISOString()
64837
+ ]
64838
+ );
64839
+ }
64840
+ async function hydrateMemberConfigFromCloud(configPath, dbUrl, encryptionKey) {
64841
+ if (!isPostgresUrl(dbUrl)) return false;
64842
+ const existing = loadConfigDoc(configPath).toJSON();
64843
+ if (Object.keys(existing.entities ?? {}).length > 0) return false;
64844
+ try {
64845
+ const peek = new Lattice({ config: configPath }, { encryptionKey });
64846
+ try {
64847
+ await peek.init({ introspectOnly: true });
64848
+ const reg = await getAsyncOrSync(
64849
+ peek.adapter,
64850
+ "SELECT to_regclass('__lattice_shared_schema') AS reg"
64851
+ );
64852
+ if (reg?.reg == null) return false;
64853
+ const row = await getAsyncOrSync(
64854
+ peek.adapter,
64855
+ 'SELECT "entities_json","contexts_json" FROM "__lattice_shared_schema" WHERE "id" = $1',
64856
+ ["singleton"]
64857
+ );
64858
+ if (row?.entities_json == null) return false;
64859
+ const entities = JSON.parse(row.entities_json);
64860
+ if (Object.keys(entities).length === 0) return false;
64861
+ const doc = loadConfigDoc(configPath);
64862
+ doc.setIn(["entities"], entities);
64863
+ if (row.contexts_json != null) {
64864
+ const ctx = JSON.parse(row.contexts_json);
64865
+ if (ctx) doc.setIn(["entityContexts"], ctx);
64866
+ }
64867
+ saveConfigDoc(configPath, doc);
64868
+ return true;
64869
+ } finally {
64870
+ peek.close();
64871
+ }
64872
+ } catch (e6) {
64873
+ console.warn(
64874
+ "[hydrateMemberConfigFromCloud] could not hydrate member schema:",
64875
+ e6.message
64876
+ );
64877
+ return false;
64878
+ }
64879
+ }
64880
+
64755
64881
  // src/gui/meta-gen.ts
64756
64882
  init_assistant_routes();
64757
64883
  init_chat();
@@ -64840,21 +64966,6 @@ var FeedBus = class {
64840
64966
  init_fts();
64841
64967
  init_mutations();
64842
64968
 
64843
- // src/gui/config-io.ts
64844
- var import_node_fs29 = require("fs");
64845
- var import_yaml4 = require("yaml");
64846
- async function execSql(db, sql) {
64847
- const adapter = db._adapter;
64848
- if (!adapter.runAsync) throw new Error("Adapter does not support runAsync");
64849
- await adapter.runAsync(sql);
64850
- }
64851
- function loadConfigDoc(configPath) {
64852
- return (0, import_yaml4.parseDocument)((0, import_node_fs29.readFileSync)(configPath, "utf8"));
64853
- }
64854
- function saveConfigDoc(configPath, doc) {
64855
- (0, import_node_fs29.writeFileSync)(configPath, doc.toString(), "utf8");
64856
- }
64857
-
64858
64969
  // src/gui/schema-ops.ts
64859
64970
  init_parser();
64860
64971
  init_canonical_context();
@@ -66043,6 +66154,7 @@ async function dispatchDbConfigRoute(req, res, ctx) {
66043
66154
  const result = await migrateLatticeData(ctx.db, target);
66044
66155
  await target.rebuildFtsIndexes();
66045
66156
  await secureCloud(target);
66157
+ await publishSharedSchema(target, ctx.configPath);
66046
66158
  target.close();
66047
66159
  const sourceDbPath = parseConfigFile(ctx.configPath).dbPath;
66048
66160
  const backupPath = archiveLocalSqlite(sourceDbPath);
@@ -67751,10 +67863,13 @@ function resolveOutputDirForConfig(configPath) {
67751
67863
  async function openConfig(configPath, outputDir, autoRender = false, realtimeWatchdogMs) {
67752
67864
  healRawDbUrl(configPath);
67753
67865
  const parsed = parseConfigFile(configPath);
67866
+ const encryptionKey = getOrCreateMasterKey();
67754
67867
  if (!/^postgres(ql)?:\/\//i.test(parsed.dbPath) && !parsed.dbPath.startsWith("file:") && parsed.dbPath !== ":memory:") {
67755
67868
  (0, import_node_fs35.mkdirSync)((0, import_node_path38.dirname)(parsed.dbPath), { recursive: true });
67756
67869
  }
67757
- const encryptionKey = getOrCreateMasterKey();
67870
+ if (/^postgres(ql)?:\/\//i.test(parsed.dbPath)) {
67871
+ await hydrateMemberConfigFromCloud(configPath, parsed.dbPath, encryptionKey);
67872
+ }
67758
67873
  const db = new Lattice({ config: configPath }, { encryptionKey });
67759
67874
  registerNativeEntities(db);
67760
67875
  db.define("_lattice_gui_meta", {
@@ -67911,16 +68026,27 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
67911
68026
  await db.ensureObservationSubstrate();
67912
68027
  await enableChangelogRls(db);
67913
68028
  await enableChatPrivacyRls(db);
68029
+ await enableGuiAuditRls(db);
67914
68030
  const access = await reconcileCloudMemberAccess(db);
67915
68031
  convergeWarnings = access.skipped;
67916
68032
  for (const s2 of convergeWarnings) {
67917
68033
  console.warn(`[openConfig] cloud converge could not manage "${s2.table}": ${s2.reason}`);
67918
68034
  }
68035
+ await publishSharedSchema(db, configPath);
67919
68036
  }
67920
68037
  } catch (e6) {
67921
68038
  console.error("[openConfig] cloud bootstrap converge failed:", e6.message);
67922
68039
  }
67923
68040
  }
68041
+ if (memberOpen) {
68042
+ const userTables = db.getRegisteredTableNames().filter((t8) => !t8.startsWith("_") && !discoveredJunctions.has(t8));
68043
+ if (userTables.length === 0) {
68044
+ convergeWarnings.push({
68045
+ table: "(schema)",
68046
+ reason: "No entity layout is configured for this cloud workspace yet \u2014 ask the cloud owner to open the workspace once so it publishes the schema, then reopen. Until then, render produces no context files."
68047
+ });
68048
+ }
68049
+ }
67924
68050
  const validTables = new Set(parsed.tables.map((t8) => t8.name));
67925
68051
  for (const name of db.getRegisteredTableNames()) {
67926
68052
  if (name.startsWith("__lattice_") || name.startsWith("_lattice_")) continue;
package/dist/index.js CHANGED
@@ -47371,6 +47371,33 @@ async function ownPolyfillsByGroup(db) {
47371
47371
  }
47372
47372
  }
47373
47373
  }
47374
+ async function enableGuiAuditRls(db) {
47375
+ if (!isPg(db)) return;
47376
+ const reg = await getAsyncOrSync(db.adapter, `SELECT to_regclass($1) AS reg`, [
47377
+ "_lattice_gui_audit"
47378
+ ]);
47379
+ if (reg?.reg == null) return;
47380
+ await runCloudBootstrapSql(
47381
+ db,
47382
+ `
47383
+ ALTER TABLE "_lattice_gui_audit" ENABLE ROW LEVEL SECURITY;
47384
+ ALTER TABLE "_lattice_gui_audit" FORCE ROW LEVEL SECURITY;
47385
+ DROP POLICY IF EXISTS "lattice_gui_audit_owner" ON "_lattice_gui_audit";
47386
+ DROP POLICY IF EXISTS "lattice_gui_audit_sel" ON "_lattice_gui_audit";
47387
+ CREATE POLICY "lattice_gui_audit_sel" ON "_lattice_gui_audit" FOR SELECT
47388
+ USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
47389
+ DROP POLICY IF EXISTS "lattice_gui_audit_ins" ON "_lattice_gui_audit";
47390
+ CREATE POLICY "lattice_gui_audit_ins" ON "_lattice_gui_audit" FOR INSERT
47391
+ WITH CHECK ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
47392
+ DROP POLICY IF EXISTS "lattice_gui_audit_upd" ON "_lattice_gui_audit";
47393
+ CREATE POLICY "lattice_gui_audit_upd" ON "_lattice_gui_audit" FOR UPDATE
47394
+ USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
47395
+ DROP POLICY IF EXISTS "lattice_gui_audit_del" ON "_lattice_gui_audit";
47396
+ CREATE POLICY "lattice_gui_audit_del" ON "_lattice_gui_audit" FOR DELETE
47397
+ USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
47398
+ `
47399
+ );
47400
+ }
47374
47401
  async function installCloudRls(db) {
47375
47402
  if (!isPg(db)) return;
47376
47403
  const schema = await cloudSchema(db);
@@ -47536,6 +47563,18 @@ CREATE TABLE IF NOT EXISTS "__lattice_member_invites" (
47536
47563
  -- bootstrap is now run directly + idempotently, not version-gated).
47537
47564
  ALTER TABLE "__lattice_member_invites" ADD COLUMN IF NOT EXISTS "email" text;
47538
47565
 
47566
+ -- Owner-published entity/render LAYOUT (the entities + entityContexts config
47567
+ -- blocks), so a joined member \u2014 whose generated config has entities: {} \u2014 can
47568
+ -- hydrate the full render layout and produce a complete context tree. This holds
47569
+ -- schema CONFIG, not row data, so it is safe to share with members (granted
47570
+ -- SELECT). A shared singleton, like __lattice_user_identity: no per-row RLS.
47571
+ CREATE TABLE IF NOT EXISTS "__lattice_shared_schema" (
47572
+ "id" TEXT PRIMARY KEY DEFAULT 'singleton',
47573
+ "entities_json" TEXT,
47574
+ "contexts_json" TEXT,
47575
+ "updated_at" TEXT
47576
+ );
47577
+
47539
47578
  -- #3.1 \u2014 one-time-use + revocation enforcement. After a member authenticates to
47540
47579
  -- the cloud with their minted credential, the join path calls this to CLAIM the
47541
47580
  -- invite. The single atomic UPDATE stamps redeemed_at and returns true ONLY when
@@ -50452,7 +50491,7 @@ var init_registry = __esm({
50452
50491
  });
50453
50492
 
50454
50493
  // src/gui/ai/lattice-docs.ts
50455
- import { readFileSync as readFileSync16, readdirSync as readdirSync5, existsSync as existsSync20 } from "fs";
50494
+ import { readFileSync as readFileSync17, readdirSync as readdirSync5, existsSync as existsSync20 } from "fs";
50456
50495
  import { dirname as dirname8, join as join25 } from "path";
50457
50496
  import { fileURLToPath as fileURLToPath2 } from "url";
50458
50497
  function findDocsDir() {
@@ -50512,7 +50551,7 @@ function allSections() {
50512
50551
  }
50513
50552
  for (const f6 of files) {
50514
50553
  try {
50515
- out.push(...sectionsOf(f6, readFileSync16(join25(dir, f6), "utf8")));
50554
+ out.push(...sectionsOf(f6, readFileSync17(join25(dir, f6), "utf8")));
50516
50555
  } catch {
50517
50556
  }
50518
50557
  }
@@ -54450,7 +54489,7 @@ var MEMBER_READABLE_BOOKKEEPING = [
54450
54489
  {
54451
54490
  name: "_lattice_gui_audit",
54452
54491
  privs: "SELECT, INSERT",
54453
- why: "the member's own GUI undo/redo log (session-scoped)"
54492
+ why: "GUI undo/redo + version history; RLS (enableGuiAuditRls) scopes reads to entries whose underlying row the member can see"
54454
54493
  },
54455
54494
  {
54456
54495
  name: "__lattice_user_identity",
@@ -54461,6 +54500,11 @@ var MEMBER_READABLE_BOOKKEEPING = [
54461
54500
  name: "__lattice_changelog",
54462
54501
  privs: "SELECT, INSERT",
54463
54502
  why: "per-viewer-RLS-filtered change history for observe()/history (the policy filters reads, so the base grant is safe)"
54503
+ },
54504
+ {
54505
+ name: "__lattice_shared_schema",
54506
+ privs: "SELECT",
54507
+ why: "owner-published entity/render layout (entities + entityContexts) a joined member hydrates its config from so render produces the full context tree"
54464
54508
  }
54465
54509
  ];
54466
54510
  var MEMBER_EXECUTE_FUNCTIONS = [
@@ -54617,6 +54661,7 @@ async function secureCloud(db) {
54617
54661
  await db.ensureObservationSubstrate();
54618
54662
  await enableChangelogRls(db);
54619
54663
  await enableChatPrivacyRls(db);
54664
+ await enableGuiAuditRls(db);
54620
54665
  await convergeLegacyColumnAudience(db);
54621
54666
  const registered = db.getRegisteredTableNames();
54622
54667
  for (const table of registered) {
@@ -64576,6 +64621,87 @@ init_postgres();
64576
64621
  init_cloud_connect();
64577
64622
  init_rls();
64578
64623
 
64624
+ // src/cloud/shared-schema.ts
64625
+ init_lattice();
64626
+ init_adapter();
64627
+
64628
+ // src/gui/config-io.ts
64629
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync5 } from "fs";
64630
+ import { parseDocument as parseDocument2 } from "yaml";
64631
+ async function execSql(db, sql) {
64632
+ const adapter = db._adapter;
64633
+ if (!adapter.runAsync) throw new Error("Adapter does not support runAsync");
64634
+ await adapter.runAsync(sql);
64635
+ }
64636
+ function loadConfigDoc(configPath) {
64637
+ return parseDocument2(readFileSync16(configPath, "utf8"));
64638
+ }
64639
+ function saveConfigDoc(configPath, doc) {
64640
+ writeFileSync5(configPath, doc.toString(), "utf8");
64641
+ }
64642
+
64643
+ // src/cloud/shared-schema.ts
64644
+ async function publishSharedSchema(db, configPath) {
64645
+ if (db.getDialect() !== "postgres") return;
64646
+ const cfg = loadConfigDoc(configPath).toJSON();
64647
+ const entities = cfg.entities ?? {};
64648
+ if (Object.keys(entities).length === 0) return;
64649
+ await runAsyncOrSync(
64650
+ db.adapter,
64651
+ `INSERT INTO "__lattice_shared_schema" ("id","entities_json","contexts_json","updated_at")
64652
+ VALUES ('singleton', $1, $2, $3)
64653
+ ON CONFLICT ("id") DO UPDATE SET
64654
+ "entities_json" = EXCLUDED."entities_json",
64655
+ "contexts_json" = EXCLUDED."contexts_json",
64656
+ "updated_at" = EXCLUDED."updated_at"`,
64657
+ [
64658
+ JSON.stringify(entities),
64659
+ JSON.stringify(cfg.entityContexts ?? null),
64660
+ (/* @__PURE__ */ new Date()).toISOString()
64661
+ ]
64662
+ );
64663
+ }
64664
+ async function hydrateMemberConfigFromCloud(configPath, dbUrl, encryptionKey) {
64665
+ if (!isPostgresUrl(dbUrl)) return false;
64666
+ const existing = loadConfigDoc(configPath).toJSON();
64667
+ if (Object.keys(existing.entities ?? {}).length > 0) return false;
64668
+ try {
64669
+ const peek = new Lattice({ config: configPath }, { encryptionKey });
64670
+ try {
64671
+ await peek.init({ introspectOnly: true });
64672
+ const reg = await getAsyncOrSync(
64673
+ peek.adapter,
64674
+ "SELECT to_regclass('__lattice_shared_schema') AS reg"
64675
+ );
64676
+ if (reg?.reg == null) return false;
64677
+ const row = await getAsyncOrSync(
64678
+ peek.adapter,
64679
+ 'SELECT "entities_json","contexts_json" FROM "__lattice_shared_schema" WHERE "id" = $1',
64680
+ ["singleton"]
64681
+ );
64682
+ if (row?.entities_json == null) return false;
64683
+ const entities = JSON.parse(row.entities_json);
64684
+ if (Object.keys(entities).length === 0) return false;
64685
+ const doc = loadConfigDoc(configPath);
64686
+ doc.setIn(["entities"], entities);
64687
+ if (row.contexts_json != null) {
64688
+ const ctx = JSON.parse(row.contexts_json);
64689
+ if (ctx) doc.setIn(["entityContexts"], ctx);
64690
+ }
64691
+ saveConfigDoc(configPath, doc);
64692
+ return true;
64693
+ } finally {
64694
+ peek.close();
64695
+ }
64696
+ } catch (e6) {
64697
+ console.warn(
64698
+ "[hydrateMemberConfigFromCloud] could not hydrate member schema:",
64699
+ e6.message
64700
+ );
64701
+ return false;
64702
+ }
64703
+ }
64704
+
64579
64705
  // src/gui/meta-gen.ts
64580
64706
  init_assistant_routes();
64581
64707
  init_chat();
@@ -64664,21 +64790,6 @@ var FeedBus = class {
64664
64790
  init_fts();
64665
64791
  init_mutations();
64666
64792
 
64667
- // src/gui/config-io.ts
64668
- import { readFileSync as readFileSync17, writeFileSync as writeFileSync5 } from "fs";
64669
- import { parseDocument as parseDocument2 } from "yaml";
64670
- async function execSql(db, sql) {
64671
- const adapter = db._adapter;
64672
- if (!adapter.runAsync) throw new Error("Adapter does not support runAsync");
64673
- await adapter.runAsync(sql);
64674
- }
64675
- function loadConfigDoc(configPath) {
64676
- return parseDocument2(readFileSync17(configPath, "utf8"));
64677
- }
64678
- function saveConfigDoc(configPath, doc) {
64679
- writeFileSync5(configPath, doc.toString(), "utf8");
64680
- }
64681
-
64682
64793
  // src/gui/schema-ops.ts
64683
64794
  init_parser();
64684
64795
  init_canonical_context();
@@ -65867,6 +65978,7 @@ async function dispatchDbConfigRoute(req, res, ctx) {
65867
65978
  const result = await migrateLatticeData(ctx.db, target);
65868
65979
  await target.rebuildFtsIndexes();
65869
65980
  await secureCloud(target);
65981
+ await publishSharedSchema(target, ctx.configPath);
65870
65982
  target.close();
65871
65983
  const sourceDbPath = parseConfigFile(ctx.configPath).dbPath;
65872
65984
  const backupPath = archiveLocalSqlite(sourceDbPath);
@@ -67575,10 +67687,13 @@ function resolveOutputDirForConfig(configPath) {
67575
67687
  async function openConfig(configPath, outputDir, autoRender = false, realtimeWatchdogMs) {
67576
67688
  healRawDbUrl(configPath);
67577
67689
  const parsed = parseConfigFile(configPath);
67690
+ const encryptionKey = getOrCreateMasterKey();
67578
67691
  if (!/^postgres(ql)?:\/\//i.test(parsed.dbPath) && !parsed.dbPath.startsWith("file:") && parsed.dbPath !== ":memory:") {
67579
67692
  mkdirSync10(dirname13(parsed.dbPath), { recursive: true });
67580
67693
  }
67581
- const encryptionKey = getOrCreateMasterKey();
67694
+ if (/^postgres(ql)?:\/\//i.test(parsed.dbPath)) {
67695
+ await hydrateMemberConfigFromCloud(configPath, parsed.dbPath, encryptionKey);
67696
+ }
67582
67697
  const db = new Lattice({ config: configPath }, { encryptionKey });
67583
67698
  registerNativeEntities(db);
67584
67699
  db.define("_lattice_gui_meta", {
@@ -67735,16 +67850,27 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
67735
67850
  await db.ensureObservationSubstrate();
67736
67851
  await enableChangelogRls(db);
67737
67852
  await enableChatPrivacyRls(db);
67853
+ await enableGuiAuditRls(db);
67738
67854
  const access = await reconcileCloudMemberAccess(db);
67739
67855
  convergeWarnings = access.skipped;
67740
67856
  for (const s2 of convergeWarnings) {
67741
67857
  console.warn(`[openConfig] cloud converge could not manage "${s2.table}": ${s2.reason}`);
67742
67858
  }
67859
+ await publishSharedSchema(db, configPath);
67743
67860
  }
67744
67861
  } catch (e6) {
67745
67862
  console.error("[openConfig] cloud bootstrap converge failed:", e6.message);
67746
67863
  }
67747
67864
  }
67865
+ if (memberOpen) {
67866
+ const userTables = db.getRegisteredTableNames().filter((t8) => !t8.startsWith("_") && !discoveredJunctions.has(t8));
67867
+ if (userTables.length === 0) {
67868
+ convergeWarnings.push({
67869
+ table: "(schema)",
67870
+ reason: "No entity layout is configured for this cloud workspace yet \u2014 ask the cloud owner to open the workspace once so it publishes the schema, then reopen. Until then, render produces no context files."
67871
+ });
67872
+ }
67873
+ }
67748
67874
  const validTables = new Set(parsed.tables.map((t8) => t8.name));
67749
67875
  for (const name of db.getRegisteredTableNames()) {
67750
67876
  if (name.startsWith("__lattice_") || name.startsWith("_lattice_")) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latticesql",
3
- "version": "3.4.1",
3
+ "version": "3.4.2",
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",