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