latticesql 3.4.1 → 3.4.3
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 +235 -24
- package/dist/index.cjs +232 -23
- package/dist/index.js +229 -20
- 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
|
}
|
|
@@ -54484,6 +54523,25 @@ var css = `
|
|
|
54484
54523
|
animation: feedSpin 0.7s linear infinite; vertical-align: middle;
|
|
54485
54524
|
}
|
|
54486
54525
|
@keyframes feedSpin { to { transform: rotate(360deg); } }
|
|
54526
|
+
/* Batch-upload progress bar \u2014 pinned to the top of the feed while a
|
|
54527
|
+
multi-file drop drains through the bounded-concurrency queue. */
|
|
54528
|
+
.ingest-progress {
|
|
54529
|
+
position: sticky; top: 0; z-index: 3;
|
|
54530
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
54531
|
+
padding: 8px 10px; border-radius: 8px;
|
|
54532
|
+
background: var(--surface); border: 1px solid rgba(190, 242, 100, 0.22);
|
|
54533
|
+
box-shadow: var(--shadow-1), var(--glow-accent-soft);
|
|
54534
|
+
}
|
|
54535
|
+
.ingest-progress-label { font-size: 12px; font-weight: 500; color: var(--text); }
|
|
54536
|
+
.ingest-progress-track {
|
|
54537
|
+
height: 6px; border-radius: 999px; overflow: hidden; background: var(--border-strong);
|
|
54538
|
+
}
|
|
54539
|
+
.ingest-progress-fill {
|
|
54540
|
+
height: 100%; width: 0%; border-radius: 999px;
|
|
54541
|
+
background: linear-gradient(90deg, var(--accent-deep), var(--accent));
|
|
54542
|
+
box-shadow: 0 0 8px rgba(190, 242, 100, 0.5);
|
|
54543
|
+
transition: width 0.3s ease;
|
|
54544
|
+
}
|
|
54487
54545
|
.assistant-rail {
|
|
54488
54546
|
position: relative;
|
|
54489
54547
|
background:
|
|
@@ -61820,6 +61878,63 @@ var appJs = `
|
|
|
61820
61878
|
// Browsers can't expose the local path, so we POST the bytes; the
|
|
61821
61879
|
// server extracts + summarizes, then discards them (path stays null).
|
|
61822
61880
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
61881
|
+
// Cap how many uploads are in flight at once. A browser allows only ~6
|
|
61882
|
+
// HTTP/1.1 connections per host, so a bulk drop of N files would fire N
|
|
61883
|
+
// upload POSTs in parallel and saturate that budget \u2014 every other data
|
|
61884
|
+
// request (entities, rows, navigation) then queues for minutes behind the
|
|
61885
|
+
// multi-minute ingests and the GUI looks frozen. Holding uploads to a few
|
|
61886
|
+
// at a time leaves connections free for the rest of the app (and eases the
|
|
61887
|
+
// AI rate limit each ingest hits server-side). The realtime/feed streams are
|
|
61888
|
+
// already off this budget \u2014 they share one WebSocket \u2014 so this is the last
|
|
61889
|
+
// place a big batch could starve the connection pool.
|
|
61890
|
+
var INGEST_MAX_CONCURRENCY = 3;
|
|
61891
|
+
// Run a batch of upload thunks with at most \`limit\` in flight, calling
|
|
61892
|
+
// onProgress(done, total) as each settles. One failure never stalls the
|
|
61893
|
+
// batch \u2014 uploadFile surfaces its own error and resolves.
|
|
61894
|
+
function runIngestBatch(thunks, limit, onProgress) {
|
|
61895
|
+
return new Promise(function (resolve) {
|
|
61896
|
+
var total = thunks.length, idx = 0, done = 0;
|
|
61897
|
+
function startNext() {
|
|
61898
|
+
if (idx >= total) return;
|
|
61899
|
+
var thunk = thunks[idx++];
|
|
61900
|
+
Promise.resolve().then(thunk).catch(function () { /* already surfaced */ }).then(function () {
|
|
61901
|
+
done++;
|
|
61902
|
+
if (onProgress) onProgress(done, total);
|
|
61903
|
+
if (done === total) resolve(); else startNext();
|
|
61904
|
+
});
|
|
61905
|
+
}
|
|
61906
|
+
for (var i = 0; i < Math.min(limit, total); i++) startNext();
|
|
61907
|
+
});
|
|
61908
|
+
}
|
|
61909
|
+
// A batch-upload progress bar pinned to the top of the rail feed
|
|
61910
|
+
// ("Analyzing N of M\u2026"). The per-file "Analyzing <name>\u2026" cards still
|
|
61911
|
+
// appear, but only INGEST_MAX_CONCURRENCY at a time; this gives the
|
|
61912
|
+
// whole-batch view that the individual cards can't. Returns
|
|
61913
|
+
// { update(done, total), done() }.
|
|
61914
|
+
function ingestProgress(total) {
|
|
61915
|
+
var feedEl = document.getElementById('rail-feed');
|
|
61916
|
+
if (!feedEl) return { update: function () {}, done: function () {} };
|
|
61917
|
+
railEmptyGone();
|
|
61918
|
+
var wrap = document.createElement('div');
|
|
61919
|
+
wrap.className = 'ingest-progress';
|
|
61920
|
+
wrap.innerHTML =
|
|
61921
|
+
'<div class="ingest-progress-label">Analyzing 0 of ' + total + '\u2026</div>' +
|
|
61922
|
+
'<div class="ingest-progress-track"><div class="ingest-progress-fill"></div></div>';
|
|
61923
|
+
feedEl.insertBefore(wrap, feedEl.firstChild);
|
|
61924
|
+
var label = wrap.querySelector('.ingest-progress-label');
|
|
61925
|
+
var fill = wrap.querySelector('.ingest-progress-fill');
|
|
61926
|
+
return {
|
|
61927
|
+
update: function (n, t) {
|
|
61928
|
+
if (label) label.textContent = 'Analyzing ' + n + ' of ' + t + '\u2026';
|
|
61929
|
+
if (fill) fill.style.width = Math.round((n / t) * 100) + '%';
|
|
61930
|
+
},
|
|
61931
|
+
done: function () {
|
|
61932
|
+
if (fill) fill.style.width = '100%';
|
|
61933
|
+
if (label) label.textContent = 'Analyzed ' + total + ' file' + (total === 1 ? '' : 's');
|
|
61934
|
+
setTimeout(function () { if (wrap.parentNode) wrap.parentNode.removeChild(wrap); }, 2500);
|
|
61935
|
+
},
|
|
61936
|
+
};
|
|
61937
|
+
}
|
|
61823
61938
|
// Append a transient "Analyzing <file>\u2026" row to the feed so the user sees
|
|
61824
61939
|
// the ingest is processing in the background; returns a disposer. The real
|
|
61825
61940
|
// create/link feed events stream in over SSE as the server materializes them.
|
|
@@ -61879,7 +61994,14 @@ var appJs = `
|
|
|
61879
61994
|
});
|
|
61880
61995
|
return;
|
|
61881
61996
|
}
|
|
61882
|
-
|
|
61997
|
+
// Multi-file: drain through the bounded-concurrency queue (so a big drop
|
|
61998
|
+
// can't saturate the connection budget) with a batch progress bar.
|
|
61999
|
+
var bar = ingestProgress(files.length);
|
|
62000
|
+
var thunks = [];
|
|
62001
|
+
for (var i = 0; i < files.length; i++) {
|
|
62002
|
+
(function (f) { thunks.push(function () { return uploadFile(f); }); })(files[i]);
|
|
62003
|
+
}
|
|
62004
|
+
runIngestBatch(thunks, INGEST_MAX_CONCURRENCY, bar.update).then(bar.done);
|
|
61883
62005
|
}
|
|
61884
62006
|
// Mobile: tapping the handle expands/collapses the bottom drawer.
|
|
61885
62007
|
function initRailDrawer() {
|
|
@@ -62767,6 +62889,87 @@ async function discoverCloudTables(db) {
|
|
|
62767
62889
|
// src/gui/server.ts
|
|
62768
62890
|
init_rls();
|
|
62769
62891
|
|
|
62892
|
+
// src/cloud/shared-schema.ts
|
|
62893
|
+
init_lattice();
|
|
62894
|
+
init_adapter();
|
|
62895
|
+
|
|
62896
|
+
// src/gui/config-io.ts
|
|
62897
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
|
|
62898
|
+
import { parseDocument as parseDocument2 } from "yaml";
|
|
62899
|
+
async function execSql(db, sql) {
|
|
62900
|
+
const adapter = db._adapter;
|
|
62901
|
+
if (!adapter.runAsync) throw new Error("Adapter does not support runAsync");
|
|
62902
|
+
await adapter.runAsync(sql);
|
|
62903
|
+
}
|
|
62904
|
+
function loadConfigDoc(configPath) {
|
|
62905
|
+
return parseDocument2(readFileSync13(configPath, "utf8"));
|
|
62906
|
+
}
|
|
62907
|
+
function saveConfigDoc(configPath, doc) {
|
|
62908
|
+
writeFileSync6(configPath, doc.toString(), "utf8");
|
|
62909
|
+
}
|
|
62910
|
+
|
|
62911
|
+
// src/cloud/shared-schema.ts
|
|
62912
|
+
async function publishSharedSchema(db, configPath) {
|
|
62913
|
+
if (db.getDialect() !== "postgres") return;
|
|
62914
|
+
const cfg = loadConfigDoc(configPath).toJSON();
|
|
62915
|
+
const entities = cfg.entities ?? {};
|
|
62916
|
+
if (Object.keys(entities).length === 0) return;
|
|
62917
|
+
await runAsyncOrSync(
|
|
62918
|
+
db.adapter,
|
|
62919
|
+
`INSERT INTO "__lattice_shared_schema" ("id","entities_json","contexts_json","updated_at")
|
|
62920
|
+
VALUES ('singleton', $1, $2, $3)
|
|
62921
|
+
ON CONFLICT ("id") DO UPDATE SET
|
|
62922
|
+
"entities_json" = EXCLUDED."entities_json",
|
|
62923
|
+
"contexts_json" = EXCLUDED."contexts_json",
|
|
62924
|
+
"updated_at" = EXCLUDED."updated_at"`,
|
|
62925
|
+
[
|
|
62926
|
+
JSON.stringify(entities),
|
|
62927
|
+
JSON.stringify(cfg.entityContexts ?? null),
|
|
62928
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
62929
|
+
]
|
|
62930
|
+
);
|
|
62931
|
+
}
|
|
62932
|
+
async function hydrateMemberConfigFromCloud(configPath, dbUrl, encryptionKey) {
|
|
62933
|
+
if (!isPostgresUrl(dbUrl)) return false;
|
|
62934
|
+
const existing = loadConfigDoc(configPath).toJSON();
|
|
62935
|
+
if (Object.keys(existing.entities ?? {}).length > 0) return false;
|
|
62936
|
+
try {
|
|
62937
|
+
const peek = new Lattice({ config: configPath }, { encryptionKey });
|
|
62938
|
+
try {
|
|
62939
|
+
await peek.init({ introspectOnly: true });
|
|
62940
|
+
const reg = await getAsyncOrSync(
|
|
62941
|
+
peek.adapter,
|
|
62942
|
+
"SELECT to_regclass('__lattice_shared_schema') AS reg"
|
|
62943
|
+
);
|
|
62944
|
+
if (reg?.reg == null) return false;
|
|
62945
|
+
const row = await getAsyncOrSync(
|
|
62946
|
+
peek.adapter,
|
|
62947
|
+
'SELECT "entities_json","contexts_json" FROM "__lattice_shared_schema" WHERE "id" = $1',
|
|
62948
|
+
["singleton"]
|
|
62949
|
+
);
|
|
62950
|
+
if (row?.entities_json == null) return false;
|
|
62951
|
+
const entities = JSON.parse(row.entities_json);
|
|
62952
|
+
if (Object.keys(entities).length === 0) return false;
|
|
62953
|
+
const doc = loadConfigDoc(configPath);
|
|
62954
|
+
doc.setIn(["entities"], entities);
|
|
62955
|
+
if (row.contexts_json != null) {
|
|
62956
|
+
const ctx = JSON.parse(row.contexts_json);
|
|
62957
|
+
if (ctx) doc.setIn(["entityContexts"], ctx);
|
|
62958
|
+
}
|
|
62959
|
+
saveConfigDoc(configPath, doc);
|
|
62960
|
+
return true;
|
|
62961
|
+
} finally {
|
|
62962
|
+
peek.close();
|
|
62963
|
+
}
|
|
62964
|
+
} catch (e6) {
|
|
62965
|
+
console.warn(
|
|
62966
|
+
"[hydrateMemberConfigFromCloud] could not hydrate member schema:",
|
|
62967
|
+
e6.message
|
|
62968
|
+
);
|
|
62969
|
+
return false;
|
|
62970
|
+
}
|
|
62971
|
+
}
|
|
62972
|
+
|
|
62770
62973
|
// src/cloud/settings.ts
|
|
62771
62974
|
init_adapter();
|
|
62772
62975
|
init_rls();
|
|
@@ -62870,7 +63073,7 @@ var MEMBER_READABLE_BOOKKEEPING = [
|
|
|
62870
63073
|
{
|
|
62871
63074
|
name: "_lattice_gui_audit",
|
|
62872
63075
|
privs: "SELECT, INSERT",
|
|
62873
|
-
why: "
|
|
63076
|
+
why: "GUI undo/redo + version history; RLS (enableGuiAuditRls) scopes reads to entries whose underlying row the member can see"
|
|
62874
63077
|
},
|
|
62875
63078
|
{
|
|
62876
63079
|
name: "__lattice_user_identity",
|
|
@@ -62881,6 +63084,11 @@ var MEMBER_READABLE_BOOKKEEPING = [
|
|
|
62881
63084
|
name: "__lattice_changelog",
|
|
62882
63085
|
privs: "SELECT, INSERT",
|
|
62883
63086
|
why: "per-viewer-RLS-filtered change history for observe()/history (the policy filters reads, so the base grant is safe)"
|
|
63087
|
+
},
|
|
63088
|
+
{
|
|
63089
|
+
name: "__lattice_shared_schema",
|
|
63090
|
+
privs: "SELECT",
|
|
63091
|
+
why: "owner-published entity/render layout (entities + entityContexts) a joined member hydrates its config from so render produces the full context tree"
|
|
62884
63092
|
}
|
|
62885
63093
|
];
|
|
62886
63094
|
var MEMBER_EXECUTE_FUNCTIONS = [
|
|
@@ -63037,6 +63245,7 @@ async function secureCloud(db) {
|
|
|
63037
63245
|
await db.ensureObservationSubstrate();
|
|
63038
63246
|
await enableChangelogRls(db);
|
|
63039
63247
|
await enableChatPrivacyRls(db);
|
|
63248
|
+
await enableGuiAuditRls(db);
|
|
63040
63249
|
await convergeLegacyColumnAudience(db);
|
|
63041
63250
|
const registered = db.getRegisteredTableNames();
|
|
63042
63251
|
for (const table of registered) {
|
|
@@ -63133,21 +63342,6 @@ var FeedBus = class {
|
|
|
63133
63342
|
init_fts();
|
|
63134
63343
|
init_mutations();
|
|
63135
63344
|
|
|
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
63345
|
// src/gui/schema-ops.ts
|
|
63152
63346
|
init_parser();
|
|
63153
63347
|
init_canonical_context();
|
|
@@ -64683,6 +64877,7 @@ async function dispatchDbConfigRoute(req, res, ctx) {
|
|
|
64683
64877
|
const result = await migrateLatticeData(ctx.db, target);
|
|
64684
64878
|
await target.rebuildFtsIndexes();
|
|
64685
64879
|
await secureCloud(target);
|
|
64880
|
+
await publishSharedSchema(target, ctx.configPath);
|
|
64686
64881
|
target.close();
|
|
64687
64882
|
const sourceDbPath = parseConfigFile(ctx.configPath).dbPath;
|
|
64688
64883
|
const backupPath = archiveLocalSqlite(sourceDbPath);
|
|
@@ -66541,10 +66736,13 @@ function resolveOutputDirForConfig(configPath) {
|
|
|
66541
66736
|
async function openConfig(configPath, outputDir, autoRender = false, realtimeWatchdogMs) {
|
|
66542
66737
|
healRawDbUrl(configPath);
|
|
66543
66738
|
const parsed = parseConfigFile(configPath);
|
|
66739
|
+
const encryptionKey = getOrCreateMasterKey();
|
|
66544
66740
|
if (!/^postgres(ql)?:\/\//i.test(parsed.dbPath) && !parsed.dbPath.startsWith("file:") && parsed.dbPath !== ":memory:") {
|
|
66545
66741
|
mkdirSync11(dirname14(parsed.dbPath), { recursive: true });
|
|
66546
66742
|
}
|
|
66547
|
-
|
|
66743
|
+
if (/^postgres(ql)?:\/\//i.test(parsed.dbPath)) {
|
|
66744
|
+
await hydrateMemberConfigFromCloud(configPath, parsed.dbPath, encryptionKey);
|
|
66745
|
+
}
|
|
66548
66746
|
const db = new Lattice({ config: configPath }, { encryptionKey });
|
|
66549
66747
|
registerNativeEntities(db);
|
|
66550
66748
|
db.define("_lattice_gui_meta", {
|
|
@@ -66701,16 +66899,27 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
|
|
|
66701
66899
|
await db.ensureObservationSubstrate();
|
|
66702
66900
|
await enableChangelogRls(db);
|
|
66703
66901
|
await enableChatPrivacyRls(db);
|
|
66902
|
+
await enableGuiAuditRls(db);
|
|
66704
66903
|
const access = await reconcileCloudMemberAccess(db);
|
|
66705
66904
|
convergeWarnings = access.skipped;
|
|
66706
66905
|
for (const s2 of convergeWarnings) {
|
|
66707
66906
|
console.warn(`[openConfig] cloud converge could not manage "${s2.table}": ${s2.reason}`);
|
|
66708
66907
|
}
|
|
66908
|
+
await publishSharedSchema(db, configPath);
|
|
66709
66909
|
}
|
|
66710
66910
|
} catch (e6) {
|
|
66711
66911
|
console.error("[openConfig] cloud bootstrap converge failed:", e6.message);
|
|
66712
66912
|
}
|
|
66713
66913
|
}
|
|
66914
|
+
if (memberOpen) {
|
|
66915
|
+
const userTables = db.getRegisteredTableNames().filter((t8) => !t8.startsWith("_") && !discoveredJunctions.has(t8));
|
|
66916
|
+
if (userTables.length === 0) {
|
|
66917
|
+
convergeWarnings.push({
|
|
66918
|
+
table: "(schema)",
|
|
66919
|
+
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."
|
|
66920
|
+
});
|
|
66921
|
+
}
|
|
66922
|
+
}
|
|
66714
66923
|
const validTables = new Set(parsed.tables.map((t8) => t8.name));
|
|
66715
66924
|
for (const name of db.getRegisteredTableNames()) {
|
|
66716
66925
|
if (name.startsWith("__lattice_") || name.startsWith("_lattice_")) continue;
|
|
@@ -69298,7 +69507,7 @@ function printHelp() {
|
|
|
69298
69507
|
);
|
|
69299
69508
|
}
|
|
69300
69509
|
function getVersion() {
|
|
69301
|
-
if (true) return "3.4.
|
|
69510
|
+
if (true) return "3.4.3";
|
|
69302
69511
|
try {
|
|
69303
69512
|
const pkgPath = new URL("../package.json", import.meta.url).pathname;
|
|
69304
69513
|
const pkg = JSON.parse(readFileSync20(pkgPath, "utf-8"));
|
|
@@ -69366,14 +69575,17 @@ function runGenerate(args) {
|
|
|
69366
69575
|
}
|
|
69367
69576
|
async function runRender(args) {
|
|
69368
69577
|
const outputDir = resolve11(args.output);
|
|
69578
|
+
const configPath = resolve11(args.config);
|
|
69369
69579
|
let parsed;
|
|
69370
69580
|
try {
|
|
69371
|
-
parsed = parseConfigFile(
|
|
69581
|
+
parsed = parseConfigFile(configPath);
|
|
69372
69582
|
} catch (e6) {
|
|
69373
69583
|
console.error(`Error: ${e6.message}`);
|
|
69374
69584
|
process.exit(1);
|
|
69375
69585
|
}
|
|
69376
|
-
const
|
|
69586
|
+
const encryptionKey = getOrCreateMasterKey();
|
|
69587
|
+
await hydrateMemberConfigFromCloud(configPath, parsed.dbPath, encryptionKey);
|
|
69588
|
+
const db = new Lattice({ config: configPath }, { encryptionKey });
|
|
69377
69589
|
try {
|
|
69378
69590
|
await db.init();
|
|
69379
69591
|
const start = Date.now();
|
|
@@ -69389,7 +69601,6 @@ async function runRender(args) {
|
|
|
69389
69601
|
} finally {
|
|
69390
69602
|
db.close();
|
|
69391
69603
|
}
|
|
69392
|
-
void parsed;
|
|
69393
69604
|
}
|
|
69394
69605
|
async function runReconcile(args, isDryRun) {
|
|
69395
69606
|
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) {
|
|
@@ -56354,6 +56399,25 @@ var css = `
|
|
|
56354
56399
|
animation: feedSpin 0.7s linear infinite; vertical-align: middle;
|
|
56355
56400
|
}
|
|
56356
56401
|
@keyframes feedSpin { to { transform: rotate(360deg); } }
|
|
56402
|
+
/* Batch-upload progress bar \u2014 pinned to the top of the feed while a
|
|
56403
|
+
multi-file drop drains through the bounded-concurrency queue. */
|
|
56404
|
+
.ingest-progress {
|
|
56405
|
+
position: sticky; top: 0; z-index: 3;
|
|
56406
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
56407
|
+
padding: 8px 10px; border-radius: 8px;
|
|
56408
|
+
background: var(--surface); border: 1px solid rgba(190, 242, 100, 0.22);
|
|
56409
|
+
box-shadow: var(--shadow-1), var(--glow-accent-soft);
|
|
56410
|
+
}
|
|
56411
|
+
.ingest-progress-label { font-size: 12px; font-weight: 500; color: var(--text); }
|
|
56412
|
+
.ingest-progress-track {
|
|
56413
|
+
height: 6px; border-radius: 999px; overflow: hidden; background: var(--border-strong);
|
|
56414
|
+
}
|
|
56415
|
+
.ingest-progress-fill {
|
|
56416
|
+
height: 100%; width: 0%; border-radius: 999px;
|
|
56417
|
+
background: linear-gradient(90deg, var(--accent-deep), var(--accent));
|
|
56418
|
+
box-shadow: 0 0 8px rgba(190, 242, 100, 0.5);
|
|
56419
|
+
transition: width 0.3s ease;
|
|
56420
|
+
}
|
|
56357
56421
|
.assistant-rail {
|
|
56358
56422
|
position: relative;
|
|
56359
56423
|
background:
|
|
@@ -63690,6 +63754,63 @@ var appJs = `
|
|
|
63690
63754
|
// Browsers can't expose the local path, so we POST the bytes; the
|
|
63691
63755
|
// server extracts + summarizes, then discards them (path stays null).
|
|
63692
63756
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
63757
|
+
// Cap how many uploads are in flight at once. A browser allows only ~6
|
|
63758
|
+
// HTTP/1.1 connections per host, so a bulk drop of N files would fire N
|
|
63759
|
+
// upload POSTs in parallel and saturate that budget \u2014 every other data
|
|
63760
|
+
// request (entities, rows, navigation) then queues for minutes behind the
|
|
63761
|
+
// multi-minute ingests and the GUI looks frozen. Holding uploads to a few
|
|
63762
|
+
// at a time leaves connections free for the rest of the app (and eases the
|
|
63763
|
+
// AI rate limit each ingest hits server-side). The realtime/feed streams are
|
|
63764
|
+
// already off this budget \u2014 they share one WebSocket \u2014 so this is the last
|
|
63765
|
+
// place a big batch could starve the connection pool.
|
|
63766
|
+
var INGEST_MAX_CONCURRENCY = 3;
|
|
63767
|
+
// Run a batch of upload thunks with at most \`limit\` in flight, calling
|
|
63768
|
+
// onProgress(done, total) as each settles. One failure never stalls the
|
|
63769
|
+
// batch \u2014 uploadFile surfaces its own error and resolves.
|
|
63770
|
+
function runIngestBatch(thunks, limit, onProgress) {
|
|
63771
|
+
return new Promise(function (resolve) {
|
|
63772
|
+
var total = thunks.length, idx = 0, done = 0;
|
|
63773
|
+
function startNext() {
|
|
63774
|
+
if (idx >= total) return;
|
|
63775
|
+
var thunk = thunks[idx++];
|
|
63776
|
+
Promise.resolve().then(thunk).catch(function () { /* already surfaced */ }).then(function () {
|
|
63777
|
+
done++;
|
|
63778
|
+
if (onProgress) onProgress(done, total);
|
|
63779
|
+
if (done === total) resolve(); else startNext();
|
|
63780
|
+
});
|
|
63781
|
+
}
|
|
63782
|
+
for (var i = 0; i < Math.min(limit, total); i++) startNext();
|
|
63783
|
+
});
|
|
63784
|
+
}
|
|
63785
|
+
// A batch-upload progress bar pinned to the top of the rail feed
|
|
63786
|
+
// ("Analyzing N of M\u2026"). The per-file "Analyzing <name>\u2026" cards still
|
|
63787
|
+
// appear, but only INGEST_MAX_CONCURRENCY at a time; this gives the
|
|
63788
|
+
// whole-batch view that the individual cards can't. Returns
|
|
63789
|
+
// { update(done, total), done() }.
|
|
63790
|
+
function ingestProgress(total) {
|
|
63791
|
+
var feedEl = document.getElementById('rail-feed');
|
|
63792
|
+
if (!feedEl) return { update: function () {}, done: function () {} };
|
|
63793
|
+
railEmptyGone();
|
|
63794
|
+
var wrap = document.createElement('div');
|
|
63795
|
+
wrap.className = 'ingest-progress';
|
|
63796
|
+
wrap.innerHTML =
|
|
63797
|
+
'<div class="ingest-progress-label">Analyzing 0 of ' + total + '\u2026</div>' +
|
|
63798
|
+
'<div class="ingest-progress-track"><div class="ingest-progress-fill"></div></div>';
|
|
63799
|
+
feedEl.insertBefore(wrap, feedEl.firstChild);
|
|
63800
|
+
var label = wrap.querySelector('.ingest-progress-label');
|
|
63801
|
+
var fill = wrap.querySelector('.ingest-progress-fill');
|
|
63802
|
+
return {
|
|
63803
|
+
update: function (n, t) {
|
|
63804
|
+
if (label) label.textContent = 'Analyzing ' + n + ' of ' + t + '\u2026';
|
|
63805
|
+
if (fill) fill.style.width = Math.round((n / t) * 100) + '%';
|
|
63806
|
+
},
|
|
63807
|
+
done: function () {
|
|
63808
|
+
if (fill) fill.style.width = '100%';
|
|
63809
|
+
if (label) label.textContent = 'Analyzed ' + total + ' file' + (total === 1 ? '' : 's');
|
|
63810
|
+
setTimeout(function () { if (wrap.parentNode) wrap.parentNode.removeChild(wrap); }, 2500);
|
|
63811
|
+
},
|
|
63812
|
+
};
|
|
63813
|
+
}
|
|
63693
63814
|
// Append a transient "Analyzing <file>\u2026" row to the feed so the user sees
|
|
63694
63815
|
// the ingest is processing in the background; returns a disposer. The real
|
|
63695
63816
|
// create/link feed events stream in over SSE as the server materializes them.
|
|
@@ -63749,7 +63870,14 @@ var appJs = `
|
|
|
63749
63870
|
});
|
|
63750
63871
|
return;
|
|
63751
63872
|
}
|
|
63752
|
-
|
|
63873
|
+
// Multi-file: drain through the bounded-concurrency queue (so a big drop
|
|
63874
|
+
// can't saturate the connection budget) with a batch progress bar.
|
|
63875
|
+
var bar = ingestProgress(files.length);
|
|
63876
|
+
var thunks = [];
|
|
63877
|
+
for (var i = 0; i < files.length; i++) {
|
|
63878
|
+
(function (f) { thunks.push(function () { return uploadFile(f); }); })(files[i]);
|
|
63879
|
+
}
|
|
63880
|
+
runIngestBatch(thunks, INGEST_MAX_CONCURRENCY, bar.update).then(bar.done);
|
|
63753
63881
|
}
|
|
63754
63882
|
// Mobile: tapping the handle expands/collapses the bottom drawer.
|
|
63755
63883
|
function initRailDrawer() {
|
|
@@ -64752,6 +64880,87 @@ init_postgres();
|
|
|
64752
64880
|
init_cloud_connect();
|
|
64753
64881
|
init_rls();
|
|
64754
64882
|
|
|
64883
|
+
// src/cloud/shared-schema.ts
|
|
64884
|
+
init_lattice();
|
|
64885
|
+
init_adapter();
|
|
64886
|
+
|
|
64887
|
+
// src/gui/config-io.ts
|
|
64888
|
+
var import_node_fs28 = require("fs");
|
|
64889
|
+
var import_yaml4 = require("yaml");
|
|
64890
|
+
async function execSql(db, sql) {
|
|
64891
|
+
const adapter = db._adapter;
|
|
64892
|
+
if (!adapter.runAsync) throw new Error("Adapter does not support runAsync");
|
|
64893
|
+
await adapter.runAsync(sql);
|
|
64894
|
+
}
|
|
64895
|
+
function loadConfigDoc(configPath) {
|
|
64896
|
+
return (0, import_yaml4.parseDocument)((0, import_node_fs28.readFileSync)(configPath, "utf8"));
|
|
64897
|
+
}
|
|
64898
|
+
function saveConfigDoc(configPath, doc) {
|
|
64899
|
+
(0, import_node_fs28.writeFileSync)(configPath, doc.toString(), "utf8");
|
|
64900
|
+
}
|
|
64901
|
+
|
|
64902
|
+
// src/cloud/shared-schema.ts
|
|
64903
|
+
async function publishSharedSchema(db, configPath) {
|
|
64904
|
+
if (db.getDialect() !== "postgres") return;
|
|
64905
|
+
const cfg = loadConfigDoc(configPath).toJSON();
|
|
64906
|
+
const entities = cfg.entities ?? {};
|
|
64907
|
+
if (Object.keys(entities).length === 0) return;
|
|
64908
|
+
await runAsyncOrSync(
|
|
64909
|
+
db.adapter,
|
|
64910
|
+
`INSERT INTO "__lattice_shared_schema" ("id","entities_json","contexts_json","updated_at")
|
|
64911
|
+
VALUES ('singleton', $1, $2, $3)
|
|
64912
|
+
ON CONFLICT ("id") DO UPDATE SET
|
|
64913
|
+
"entities_json" = EXCLUDED."entities_json",
|
|
64914
|
+
"contexts_json" = EXCLUDED."contexts_json",
|
|
64915
|
+
"updated_at" = EXCLUDED."updated_at"`,
|
|
64916
|
+
[
|
|
64917
|
+
JSON.stringify(entities),
|
|
64918
|
+
JSON.stringify(cfg.entityContexts ?? null),
|
|
64919
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
64920
|
+
]
|
|
64921
|
+
);
|
|
64922
|
+
}
|
|
64923
|
+
async function hydrateMemberConfigFromCloud(configPath, dbUrl, encryptionKey) {
|
|
64924
|
+
if (!isPostgresUrl(dbUrl)) return false;
|
|
64925
|
+
const existing = loadConfigDoc(configPath).toJSON();
|
|
64926
|
+
if (Object.keys(existing.entities ?? {}).length > 0) return false;
|
|
64927
|
+
try {
|
|
64928
|
+
const peek = new Lattice({ config: configPath }, { encryptionKey });
|
|
64929
|
+
try {
|
|
64930
|
+
await peek.init({ introspectOnly: true });
|
|
64931
|
+
const reg = await getAsyncOrSync(
|
|
64932
|
+
peek.adapter,
|
|
64933
|
+
"SELECT to_regclass('__lattice_shared_schema') AS reg"
|
|
64934
|
+
);
|
|
64935
|
+
if (reg?.reg == null) return false;
|
|
64936
|
+
const row = await getAsyncOrSync(
|
|
64937
|
+
peek.adapter,
|
|
64938
|
+
'SELECT "entities_json","contexts_json" FROM "__lattice_shared_schema" WHERE "id" = $1',
|
|
64939
|
+
["singleton"]
|
|
64940
|
+
);
|
|
64941
|
+
if (row?.entities_json == null) return false;
|
|
64942
|
+
const entities = JSON.parse(row.entities_json);
|
|
64943
|
+
if (Object.keys(entities).length === 0) return false;
|
|
64944
|
+
const doc = loadConfigDoc(configPath);
|
|
64945
|
+
doc.setIn(["entities"], entities);
|
|
64946
|
+
if (row.contexts_json != null) {
|
|
64947
|
+
const ctx = JSON.parse(row.contexts_json);
|
|
64948
|
+
if (ctx) doc.setIn(["entityContexts"], ctx);
|
|
64949
|
+
}
|
|
64950
|
+
saveConfigDoc(configPath, doc);
|
|
64951
|
+
return true;
|
|
64952
|
+
} finally {
|
|
64953
|
+
peek.close();
|
|
64954
|
+
}
|
|
64955
|
+
} catch (e6) {
|
|
64956
|
+
console.warn(
|
|
64957
|
+
"[hydrateMemberConfigFromCloud] could not hydrate member schema:",
|
|
64958
|
+
e6.message
|
|
64959
|
+
);
|
|
64960
|
+
return false;
|
|
64961
|
+
}
|
|
64962
|
+
}
|
|
64963
|
+
|
|
64755
64964
|
// src/gui/meta-gen.ts
|
|
64756
64965
|
init_assistant_routes();
|
|
64757
64966
|
init_chat();
|
|
@@ -64840,21 +65049,6 @@ var FeedBus = class {
|
|
|
64840
65049
|
init_fts();
|
|
64841
65050
|
init_mutations();
|
|
64842
65051
|
|
|
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
65052
|
// src/gui/schema-ops.ts
|
|
64859
65053
|
init_parser();
|
|
64860
65054
|
init_canonical_context();
|
|
@@ -66043,6 +66237,7 @@ async function dispatchDbConfigRoute(req, res, ctx) {
|
|
|
66043
66237
|
const result = await migrateLatticeData(ctx.db, target);
|
|
66044
66238
|
await target.rebuildFtsIndexes();
|
|
66045
66239
|
await secureCloud(target);
|
|
66240
|
+
await publishSharedSchema(target, ctx.configPath);
|
|
66046
66241
|
target.close();
|
|
66047
66242
|
const sourceDbPath = parseConfigFile(ctx.configPath).dbPath;
|
|
66048
66243
|
const backupPath = archiveLocalSqlite(sourceDbPath);
|
|
@@ -67751,10 +67946,13 @@ function resolveOutputDirForConfig(configPath) {
|
|
|
67751
67946
|
async function openConfig(configPath, outputDir, autoRender = false, realtimeWatchdogMs) {
|
|
67752
67947
|
healRawDbUrl(configPath);
|
|
67753
67948
|
const parsed = parseConfigFile(configPath);
|
|
67949
|
+
const encryptionKey = getOrCreateMasterKey();
|
|
67754
67950
|
if (!/^postgres(ql)?:\/\//i.test(parsed.dbPath) && !parsed.dbPath.startsWith("file:") && parsed.dbPath !== ":memory:") {
|
|
67755
67951
|
(0, import_node_fs35.mkdirSync)((0, import_node_path38.dirname)(parsed.dbPath), { recursive: true });
|
|
67756
67952
|
}
|
|
67757
|
-
|
|
67953
|
+
if (/^postgres(ql)?:\/\//i.test(parsed.dbPath)) {
|
|
67954
|
+
await hydrateMemberConfigFromCloud(configPath, parsed.dbPath, encryptionKey);
|
|
67955
|
+
}
|
|
67758
67956
|
const db = new Lattice({ config: configPath }, { encryptionKey });
|
|
67759
67957
|
registerNativeEntities(db);
|
|
67760
67958
|
db.define("_lattice_gui_meta", {
|
|
@@ -67911,16 +68109,27 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
|
|
|
67911
68109
|
await db.ensureObservationSubstrate();
|
|
67912
68110
|
await enableChangelogRls(db);
|
|
67913
68111
|
await enableChatPrivacyRls(db);
|
|
68112
|
+
await enableGuiAuditRls(db);
|
|
67914
68113
|
const access = await reconcileCloudMemberAccess(db);
|
|
67915
68114
|
convergeWarnings = access.skipped;
|
|
67916
68115
|
for (const s2 of convergeWarnings) {
|
|
67917
68116
|
console.warn(`[openConfig] cloud converge could not manage "${s2.table}": ${s2.reason}`);
|
|
67918
68117
|
}
|
|
68118
|
+
await publishSharedSchema(db, configPath);
|
|
67919
68119
|
}
|
|
67920
68120
|
} catch (e6) {
|
|
67921
68121
|
console.error("[openConfig] cloud bootstrap converge failed:", e6.message);
|
|
67922
68122
|
}
|
|
67923
68123
|
}
|
|
68124
|
+
if (memberOpen) {
|
|
68125
|
+
const userTables = db.getRegisteredTableNames().filter((t8) => !t8.startsWith("_") && !discoveredJunctions.has(t8));
|
|
68126
|
+
if (userTables.length === 0) {
|
|
68127
|
+
convergeWarnings.push({
|
|
68128
|
+
table: "(schema)",
|
|
68129
|
+
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."
|
|
68130
|
+
});
|
|
68131
|
+
}
|
|
68132
|
+
}
|
|
67924
68133
|
const validTables = new Set(parsed.tables.map((t8) => t8.name));
|
|
67925
68134
|
for (const name of db.getRegisteredTableNames()) {
|
|
67926
68135
|
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) {
|
|
@@ -56179,6 +56224,25 @@ var css = `
|
|
|
56179
56224
|
animation: feedSpin 0.7s linear infinite; vertical-align: middle;
|
|
56180
56225
|
}
|
|
56181
56226
|
@keyframes feedSpin { to { transform: rotate(360deg); } }
|
|
56227
|
+
/* Batch-upload progress bar \u2014 pinned to the top of the feed while a
|
|
56228
|
+
multi-file drop drains through the bounded-concurrency queue. */
|
|
56229
|
+
.ingest-progress {
|
|
56230
|
+
position: sticky; top: 0; z-index: 3;
|
|
56231
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
56232
|
+
padding: 8px 10px; border-radius: 8px;
|
|
56233
|
+
background: var(--surface); border: 1px solid rgba(190, 242, 100, 0.22);
|
|
56234
|
+
box-shadow: var(--shadow-1), var(--glow-accent-soft);
|
|
56235
|
+
}
|
|
56236
|
+
.ingest-progress-label { font-size: 12px; font-weight: 500; color: var(--text); }
|
|
56237
|
+
.ingest-progress-track {
|
|
56238
|
+
height: 6px; border-radius: 999px; overflow: hidden; background: var(--border-strong);
|
|
56239
|
+
}
|
|
56240
|
+
.ingest-progress-fill {
|
|
56241
|
+
height: 100%; width: 0%; border-radius: 999px;
|
|
56242
|
+
background: linear-gradient(90deg, var(--accent-deep), var(--accent));
|
|
56243
|
+
box-shadow: 0 0 8px rgba(190, 242, 100, 0.5);
|
|
56244
|
+
transition: width 0.3s ease;
|
|
56245
|
+
}
|
|
56182
56246
|
.assistant-rail {
|
|
56183
56247
|
position: relative;
|
|
56184
56248
|
background:
|
|
@@ -63515,6 +63579,63 @@ var appJs = `
|
|
|
63515
63579
|
// Browsers can't expose the local path, so we POST the bytes; the
|
|
63516
63580
|
// server extracts + summarizes, then discards them (path stays null).
|
|
63517
63581
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
63582
|
+
// Cap how many uploads are in flight at once. A browser allows only ~6
|
|
63583
|
+
// HTTP/1.1 connections per host, so a bulk drop of N files would fire N
|
|
63584
|
+
// upload POSTs in parallel and saturate that budget \u2014 every other data
|
|
63585
|
+
// request (entities, rows, navigation) then queues for minutes behind the
|
|
63586
|
+
// multi-minute ingests and the GUI looks frozen. Holding uploads to a few
|
|
63587
|
+
// at a time leaves connections free for the rest of the app (and eases the
|
|
63588
|
+
// AI rate limit each ingest hits server-side). The realtime/feed streams are
|
|
63589
|
+
// already off this budget \u2014 they share one WebSocket \u2014 so this is the last
|
|
63590
|
+
// place a big batch could starve the connection pool.
|
|
63591
|
+
var INGEST_MAX_CONCURRENCY = 3;
|
|
63592
|
+
// Run a batch of upload thunks with at most \`limit\` in flight, calling
|
|
63593
|
+
// onProgress(done, total) as each settles. One failure never stalls the
|
|
63594
|
+
// batch \u2014 uploadFile surfaces its own error and resolves.
|
|
63595
|
+
function runIngestBatch(thunks, limit, onProgress) {
|
|
63596
|
+
return new Promise(function (resolve) {
|
|
63597
|
+
var total = thunks.length, idx = 0, done = 0;
|
|
63598
|
+
function startNext() {
|
|
63599
|
+
if (idx >= total) return;
|
|
63600
|
+
var thunk = thunks[idx++];
|
|
63601
|
+
Promise.resolve().then(thunk).catch(function () { /* already surfaced */ }).then(function () {
|
|
63602
|
+
done++;
|
|
63603
|
+
if (onProgress) onProgress(done, total);
|
|
63604
|
+
if (done === total) resolve(); else startNext();
|
|
63605
|
+
});
|
|
63606
|
+
}
|
|
63607
|
+
for (var i = 0; i < Math.min(limit, total); i++) startNext();
|
|
63608
|
+
});
|
|
63609
|
+
}
|
|
63610
|
+
// A batch-upload progress bar pinned to the top of the rail feed
|
|
63611
|
+
// ("Analyzing N of M\u2026"). The per-file "Analyzing <name>\u2026" cards still
|
|
63612
|
+
// appear, but only INGEST_MAX_CONCURRENCY at a time; this gives the
|
|
63613
|
+
// whole-batch view that the individual cards can't. Returns
|
|
63614
|
+
// { update(done, total), done() }.
|
|
63615
|
+
function ingestProgress(total) {
|
|
63616
|
+
var feedEl = document.getElementById('rail-feed');
|
|
63617
|
+
if (!feedEl) return { update: function () {}, done: function () {} };
|
|
63618
|
+
railEmptyGone();
|
|
63619
|
+
var wrap = document.createElement('div');
|
|
63620
|
+
wrap.className = 'ingest-progress';
|
|
63621
|
+
wrap.innerHTML =
|
|
63622
|
+
'<div class="ingest-progress-label">Analyzing 0 of ' + total + '\u2026</div>' +
|
|
63623
|
+
'<div class="ingest-progress-track"><div class="ingest-progress-fill"></div></div>';
|
|
63624
|
+
feedEl.insertBefore(wrap, feedEl.firstChild);
|
|
63625
|
+
var label = wrap.querySelector('.ingest-progress-label');
|
|
63626
|
+
var fill = wrap.querySelector('.ingest-progress-fill');
|
|
63627
|
+
return {
|
|
63628
|
+
update: function (n, t) {
|
|
63629
|
+
if (label) label.textContent = 'Analyzing ' + n + ' of ' + t + '\u2026';
|
|
63630
|
+
if (fill) fill.style.width = Math.round((n / t) * 100) + '%';
|
|
63631
|
+
},
|
|
63632
|
+
done: function () {
|
|
63633
|
+
if (fill) fill.style.width = '100%';
|
|
63634
|
+
if (label) label.textContent = 'Analyzed ' + total + ' file' + (total === 1 ? '' : 's');
|
|
63635
|
+
setTimeout(function () { if (wrap.parentNode) wrap.parentNode.removeChild(wrap); }, 2500);
|
|
63636
|
+
},
|
|
63637
|
+
};
|
|
63638
|
+
}
|
|
63518
63639
|
// Append a transient "Analyzing <file>\u2026" row to the feed so the user sees
|
|
63519
63640
|
// the ingest is processing in the background; returns a disposer. The real
|
|
63520
63641
|
// create/link feed events stream in over SSE as the server materializes them.
|
|
@@ -63574,7 +63695,14 @@ var appJs = `
|
|
|
63574
63695
|
});
|
|
63575
63696
|
return;
|
|
63576
63697
|
}
|
|
63577
|
-
|
|
63698
|
+
// Multi-file: drain through the bounded-concurrency queue (so a big drop
|
|
63699
|
+
// can't saturate the connection budget) with a batch progress bar.
|
|
63700
|
+
var bar = ingestProgress(files.length);
|
|
63701
|
+
var thunks = [];
|
|
63702
|
+
for (var i = 0; i < files.length; i++) {
|
|
63703
|
+
(function (f) { thunks.push(function () { return uploadFile(f); }); })(files[i]);
|
|
63704
|
+
}
|
|
63705
|
+
runIngestBatch(thunks, INGEST_MAX_CONCURRENCY, bar.update).then(bar.done);
|
|
63578
63706
|
}
|
|
63579
63707
|
// Mobile: tapping the handle expands/collapses the bottom drawer.
|
|
63580
63708
|
function initRailDrawer() {
|
|
@@ -64576,6 +64704,87 @@ init_postgres();
|
|
|
64576
64704
|
init_cloud_connect();
|
|
64577
64705
|
init_rls();
|
|
64578
64706
|
|
|
64707
|
+
// src/cloud/shared-schema.ts
|
|
64708
|
+
init_lattice();
|
|
64709
|
+
init_adapter();
|
|
64710
|
+
|
|
64711
|
+
// src/gui/config-io.ts
|
|
64712
|
+
import { readFileSync as readFileSync16, writeFileSync as writeFileSync5 } from "fs";
|
|
64713
|
+
import { parseDocument as parseDocument2 } from "yaml";
|
|
64714
|
+
async function execSql(db, sql) {
|
|
64715
|
+
const adapter = db._adapter;
|
|
64716
|
+
if (!adapter.runAsync) throw new Error("Adapter does not support runAsync");
|
|
64717
|
+
await adapter.runAsync(sql);
|
|
64718
|
+
}
|
|
64719
|
+
function loadConfigDoc(configPath) {
|
|
64720
|
+
return parseDocument2(readFileSync16(configPath, "utf8"));
|
|
64721
|
+
}
|
|
64722
|
+
function saveConfigDoc(configPath, doc) {
|
|
64723
|
+
writeFileSync5(configPath, doc.toString(), "utf8");
|
|
64724
|
+
}
|
|
64725
|
+
|
|
64726
|
+
// src/cloud/shared-schema.ts
|
|
64727
|
+
async function publishSharedSchema(db, configPath) {
|
|
64728
|
+
if (db.getDialect() !== "postgres") return;
|
|
64729
|
+
const cfg = loadConfigDoc(configPath).toJSON();
|
|
64730
|
+
const entities = cfg.entities ?? {};
|
|
64731
|
+
if (Object.keys(entities).length === 0) return;
|
|
64732
|
+
await runAsyncOrSync(
|
|
64733
|
+
db.adapter,
|
|
64734
|
+
`INSERT INTO "__lattice_shared_schema" ("id","entities_json","contexts_json","updated_at")
|
|
64735
|
+
VALUES ('singleton', $1, $2, $3)
|
|
64736
|
+
ON CONFLICT ("id") DO UPDATE SET
|
|
64737
|
+
"entities_json" = EXCLUDED."entities_json",
|
|
64738
|
+
"contexts_json" = EXCLUDED."contexts_json",
|
|
64739
|
+
"updated_at" = EXCLUDED."updated_at"`,
|
|
64740
|
+
[
|
|
64741
|
+
JSON.stringify(entities),
|
|
64742
|
+
JSON.stringify(cfg.entityContexts ?? null),
|
|
64743
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
64744
|
+
]
|
|
64745
|
+
);
|
|
64746
|
+
}
|
|
64747
|
+
async function hydrateMemberConfigFromCloud(configPath, dbUrl, encryptionKey) {
|
|
64748
|
+
if (!isPostgresUrl(dbUrl)) return false;
|
|
64749
|
+
const existing = loadConfigDoc(configPath).toJSON();
|
|
64750
|
+
if (Object.keys(existing.entities ?? {}).length > 0) return false;
|
|
64751
|
+
try {
|
|
64752
|
+
const peek = new Lattice({ config: configPath }, { encryptionKey });
|
|
64753
|
+
try {
|
|
64754
|
+
await peek.init({ introspectOnly: true });
|
|
64755
|
+
const reg = await getAsyncOrSync(
|
|
64756
|
+
peek.adapter,
|
|
64757
|
+
"SELECT to_regclass('__lattice_shared_schema') AS reg"
|
|
64758
|
+
);
|
|
64759
|
+
if (reg?.reg == null) return false;
|
|
64760
|
+
const row = await getAsyncOrSync(
|
|
64761
|
+
peek.adapter,
|
|
64762
|
+
'SELECT "entities_json","contexts_json" FROM "__lattice_shared_schema" WHERE "id" = $1',
|
|
64763
|
+
["singleton"]
|
|
64764
|
+
);
|
|
64765
|
+
if (row?.entities_json == null) return false;
|
|
64766
|
+
const entities = JSON.parse(row.entities_json);
|
|
64767
|
+
if (Object.keys(entities).length === 0) return false;
|
|
64768
|
+
const doc = loadConfigDoc(configPath);
|
|
64769
|
+
doc.setIn(["entities"], entities);
|
|
64770
|
+
if (row.contexts_json != null) {
|
|
64771
|
+
const ctx = JSON.parse(row.contexts_json);
|
|
64772
|
+
if (ctx) doc.setIn(["entityContexts"], ctx);
|
|
64773
|
+
}
|
|
64774
|
+
saveConfigDoc(configPath, doc);
|
|
64775
|
+
return true;
|
|
64776
|
+
} finally {
|
|
64777
|
+
peek.close();
|
|
64778
|
+
}
|
|
64779
|
+
} catch (e6) {
|
|
64780
|
+
console.warn(
|
|
64781
|
+
"[hydrateMemberConfigFromCloud] could not hydrate member schema:",
|
|
64782
|
+
e6.message
|
|
64783
|
+
);
|
|
64784
|
+
return false;
|
|
64785
|
+
}
|
|
64786
|
+
}
|
|
64787
|
+
|
|
64579
64788
|
// src/gui/meta-gen.ts
|
|
64580
64789
|
init_assistant_routes();
|
|
64581
64790
|
init_chat();
|
|
@@ -64664,21 +64873,6 @@ var FeedBus = class {
|
|
|
64664
64873
|
init_fts();
|
|
64665
64874
|
init_mutations();
|
|
64666
64875
|
|
|
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
64876
|
// src/gui/schema-ops.ts
|
|
64683
64877
|
init_parser();
|
|
64684
64878
|
init_canonical_context();
|
|
@@ -65867,6 +66061,7 @@ async function dispatchDbConfigRoute(req, res, ctx) {
|
|
|
65867
66061
|
const result = await migrateLatticeData(ctx.db, target);
|
|
65868
66062
|
await target.rebuildFtsIndexes();
|
|
65869
66063
|
await secureCloud(target);
|
|
66064
|
+
await publishSharedSchema(target, ctx.configPath);
|
|
65870
66065
|
target.close();
|
|
65871
66066
|
const sourceDbPath = parseConfigFile(ctx.configPath).dbPath;
|
|
65872
66067
|
const backupPath = archiveLocalSqlite(sourceDbPath);
|
|
@@ -67575,10 +67770,13 @@ function resolveOutputDirForConfig(configPath) {
|
|
|
67575
67770
|
async function openConfig(configPath, outputDir, autoRender = false, realtimeWatchdogMs) {
|
|
67576
67771
|
healRawDbUrl(configPath);
|
|
67577
67772
|
const parsed = parseConfigFile(configPath);
|
|
67773
|
+
const encryptionKey = getOrCreateMasterKey();
|
|
67578
67774
|
if (!/^postgres(ql)?:\/\//i.test(parsed.dbPath) && !parsed.dbPath.startsWith("file:") && parsed.dbPath !== ":memory:") {
|
|
67579
67775
|
mkdirSync10(dirname13(parsed.dbPath), { recursive: true });
|
|
67580
67776
|
}
|
|
67581
|
-
|
|
67777
|
+
if (/^postgres(ql)?:\/\//i.test(parsed.dbPath)) {
|
|
67778
|
+
await hydrateMemberConfigFromCloud(configPath, parsed.dbPath, encryptionKey);
|
|
67779
|
+
}
|
|
67582
67780
|
const db = new Lattice({ config: configPath }, { encryptionKey });
|
|
67583
67781
|
registerNativeEntities(db);
|
|
67584
67782
|
db.define("_lattice_gui_meta", {
|
|
@@ -67735,16 +67933,27 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
|
|
|
67735
67933
|
await db.ensureObservationSubstrate();
|
|
67736
67934
|
await enableChangelogRls(db);
|
|
67737
67935
|
await enableChatPrivacyRls(db);
|
|
67936
|
+
await enableGuiAuditRls(db);
|
|
67738
67937
|
const access = await reconcileCloudMemberAccess(db);
|
|
67739
67938
|
convergeWarnings = access.skipped;
|
|
67740
67939
|
for (const s2 of convergeWarnings) {
|
|
67741
67940
|
console.warn(`[openConfig] cloud converge could not manage "${s2.table}": ${s2.reason}`);
|
|
67742
67941
|
}
|
|
67942
|
+
await publishSharedSchema(db, configPath);
|
|
67743
67943
|
}
|
|
67744
67944
|
} catch (e6) {
|
|
67745
67945
|
console.error("[openConfig] cloud bootstrap converge failed:", e6.message);
|
|
67746
67946
|
}
|
|
67747
67947
|
}
|
|
67948
|
+
if (memberOpen) {
|
|
67949
|
+
const userTables = db.getRegisteredTableNames().filter((t8) => !t8.startsWith("_") && !discoveredJunctions.has(t8));
|
|
67950
|
+
if (userTables.length === 0) {
|
|
67951
|
+
convergeWarnings.push({
|
|
67952
|
+
table: "(schema)",
|
|
67953
|
+
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."
|
|
67954
|
+
});
|
|
67955
|
+
}
|
|
67956
|
+
}
|
|
67748
67957
|
const validTables = new Set(parsed.tables.map((t8) => t8.name));
|
|
67749
67958
|
for (const name of db.getRegisteredTableNames()) {
|
|
67750
67959
|
if (name.startsWith("__lattice_") || name.startsWith("_lattice_")) continue;
|
package/package.json
CHANGED