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 +151 -23
- package/dist/index.cjs +148 -22
- package/dist/index.js +145 -19
- package/package.json +1 -1
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
|
|
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,
|
|
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: "
|
|
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
|
-
|
|
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.
|
|
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(
|
|
69498
|
+
parsed = parseConfigFile(configPath);
|
|
69372
69499
|
} catch (e6) {
|
|
69373
69500
|
console.error(`Error: ${e6.message}`);
|
|
69374
69501
|
process.exit(1);
|
|
69375
69502
|
}
|
|
69376
|
-
const
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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,
|
|
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: "
|
|
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
|
-
|
|
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