latticesql 3.0.0 → 3.1.0
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 +2044 -695
- package/dist/index.cjs +561 -30
- package/dist/index.d.cts +220 -13
- package/dist/index.d.ts +220 -13
- package/dist/index.js +553 -30
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -39405,6 +39405,7 @@ __export(index_exports, {
|
|
|
39405
39405
|
NATIVE_ENTITY_NAMES: () => NATIVE_ENTITY_NAMES,
|
|
39406
39406
|
NATIVE_REGISTRY_TABLE: () => NATIVE_REGISTRY_TABLE,
|
|
39407
39407
|
PostgresAdapter: () => PostgresAdapter,
|
|
39408
|
+
ProgressThrottle: () => ProgressThrottle,
|
|
39408
39409
|
READ_ONLY_HEADER: () => READ_ONLY_HEADER,
|
|
39409
39410
|
ROOT_DIRNAME: () => ROOT_DIRNAME,
|
|
39410
39411
|
ReferenceUnavailableError: () => ReferenceUnavailableError,
|
|
@@ -39468,6 +39469,7 @@ __export(index_exports, {
|
|
|
39468
39469
|
getCloudSetting: () => getCloudSetting,
|
|
39469
39470
|
getDbCredential: () => getDbCredential,
|
|
39470
39471
|
getOrCreateMasterKey: () => getOrCreateMasterKey,
|
|
39472
|
+
getTablePolicy: () => getTablePolicy,
|
|
39471
39473
|
getWorkspace: () => getWorkspace,
|
|
39472
39474
|
grantCell: () => grantCell,
|
|
39473
39475
|
hasFtsIndex: () => hasFtsIndex,
|
|
@@ -39485,6 +39487,7 @@ __export(index_exports, {
|
|
|
39485
39487
|
listNativeBindings: () => listNativeBindings,
|
|
39486
39488
|
listTokens: () => listTokens,
|
|
39487
39489
|
listWorkspaces: () => listWorkspaces,
|
|
39490
|
+
loadColumnPolicy: () => loadColumnPolicy,
|
|
39488
39491
|
manifestPath: () => manifestPath,
|
|
39489
39492
|
markdownTable: () => markdownTable,
|
|
39490
39493
|
memberRoleName: () => memberRoleName,
|
|
@@ -39512,6 +39515,7 @@ __export(index_exports, {
|
|
|
39512
39515
|
readToken: () => readToken,
|
|
39513
39516
|
referenceLocalFile: () => referenceLocalFile,
|
|
39514
39517
|
referenceUrl: () => referenceUrl,
|
|
39518
|
+
regenerateAudienceViewFromDb: () => regenerateAudienceViewFromDb,
|
|
39515
39519
|
registerNativeEntities: () => registerNativeEntities,
|
|
39516
39520
|
registryPath: () => registryPath,
|
|
39517
39521
|
resolveActiveS3Config: () => resolveActiveS3Config,
|
|
@@ -39526,9 +39530,13 @@ __export(index_exports, {
|
|
|
39526
39530
|
saveDbCredentialForTeam: () => saveDbCredentialForTeam,
|
|
39527
39531
|
sealUnderSource: () => sealUnderSource,
|
|
39528
39532
|
secureCloud: () => secureCloud,
|
|
39533
|
+
seedColumnPolicyFromYaml: () => seedColumnPolicyFromYaml,
|
|
39529
39534
|
setActiveWorkspace: () => setActiveWorkspace,
|
|
39530
39535
|
setCloudSetting: () => setCloudSetting,
|
|
39536
|
+
setColumnAudience: () => setColumnAudience,
|
|
39531
39537
|
setRowVisibility: () => setRowVisibility,
|
|
39538
|
+
setTableDefaultVisibility: () => setTableDefaultVisibility,
|
|
39539
|
+
setTableNeverShare: () => setTableNeverShare,
|
|
39532
39540
|
shredSource: () => shredSource,
|
|
39533
39541
|
slugify: () => slugify,
|
|
39534
39542
|
summarizeText: () => summarizeText,
|
|
@@ -41616,7 +41624,42 @@ function cleanupEntityContexts(outputDir, entityContexts, currentSlugsByTable, m
|
|
|
41616
41624
|
return result;
|
|
41617
41625
|
}
|
|
41618
41626
|
|
|
41627
|
+
// src/render/progress.ts
|
|
41628
|
+
var THROTTLE_WINDOW_MS = 200;
|
|
41629
|
+
var ProgressThrottle = class {
|
|
41630
|
+
cb;
|
|
41631
|
+
windowMs;
|
|
41632
|
+
lastEmit = 0;
|
|
41633
|
+
constructor(cb, windowMs = THROTTLE_WINDOW_MS) {
|
|
41634
|
+
this.cb = cb;
|
|
41635
|
+
this.windowMs = windowMs;
|
|
41636
|
+
}
|
|
41637
|
+
/**
|
|
41638
|
+
* Emit a `table-progress` event, but only if the window since the last
|
|
41639
|
+
* passthrough has elapsed. Dropped events are simply not delivered — the next
|
|
41640
|
+
* one that survives carries the latest running count.
|
|
41641
|
+
*/
|
|
41642
|
+
tick(event) {
|
|
41643
|
+
if (!this.cb) return;
|
|
41644
|
+
const now = Date.now();
|
|
41645
|
+
if (now - this.lastEmit < this.windowMs) return;
|
|
41646
|
+
this.lastEmit = now;
|
|
41647
|
+
this.cb(event);
|
|
41648
|
+
}
|
|
41649
|
+
/**
|
|
41650
|
+
* Emit a lifecycle event immediately and reset the throttle window. Use for
|
|
41651
|
+
* `table-start`, `table-done`, `done`, and `error` — none of which should
|
|
41652
|
+
* ever be dropped. Resetting on `table-start` gives each table a clean budget.
|
|
41653
|
+
*/
|
|
41654
|
+
force(event) {
|
|
41655
|
+
if (!this.cb) return;
|
|
41656
|
+
this.lastEmit = Date.now();
|
|
41657
|
+
this.cb(event);
|
|
41658
|
+
}
|
|
41659
|
+
};
|
|
41660
|
+
|
|
41619
41661
|
// src/render/engine.ts
|
|
41662
|
+
var YIELD_EVERY_ENTITIES = 200;
|
|
41620
41663
|
var NOOP_RENDER = () => "";
|
|
41621
41664
|
var RenderEngine = class {
|
|
41622
41665
|
_schema;
|
|
@@ -41630,11 +41673,14 @@ var RenderEngine = class {
|
|
|
41630
41673
|
this._getTaskContext = getTaskContext ?? (() => "");
|
|
41631
41674
|
this._skipEmpty = options?.skipEmpty ?? false;
|
|
41632
41675
|
}
|
|
41633
|
-
async render(outputDir) {
|
|
41676
|
+
async render(outputDir, opts = {}) {
|
|
41634
41677
|
const start = Date.now();
|
|
41635
41678
|
const filesWritten = [];
|
|
41636
41679
|
const counters = { skipped: 0 };
|
|
41680
|
+
const signal = opts.signal;
|
|
41681
|
+
const throttle = new ProgressThrottle(opts.onProgress);
|
|
41637
41682
|
for (const [name, def] of this._schema.getTables()) {
|
|
41683
|
+
if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
|
|
41638
41684
|
if (this._skipEmpty && def.render === NOOP_RENDER) continue;
|
|
41639
41685
|
let rows = await this._schema.queryTable(this._adapter, name);
|
|
41640
41686
|
if (def.relevanceFilter) {
|
|
@@ -41676,8 +41722,18 @@ var RenderEngine = class {
|
|
|
41676
41722
|
} else {
|
|
41677
41723
|
counters.skipped++;
|
|
41678
41724
|
}
|
|
41725
|
+
throttle.force({
|
|
41726
|
+
kind: "table-done",
|
|
41727
|
+
table: name,
|
|
41728
|
+
entitiesRendered: rows.length,
|
|
41729
|
+
entitiesTotal: rows.length,
|
|
41730
|
+
tableIndex: 0,
|
|
41731
|
+
tableCount: 0,
|
|
41732
|
+
pct: 100
|
|
41733
|
+
});
|
|
41679
41734
|
}
|
|
41680
|
-
for (const [, def] of this._schema.getMultis()) {
|
|
41735
|
+
for (const [name, def] of this._schema.getMultis()) {
|
|
41736
|
+
if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
|
|
41681
41737
|
const keys = await def.keys();
|
|
41682
41738
|
const tables = {};
|
|
41683
41739
|
if (def.tables) {
|
|
@@ -41694,12 +41750,26 @@ var RenderEngine = class {
|
|
|
41694
41750
|
counters.skipped++;
|
|
41695
41751
|
}
|
|
41696
41752
|
}
|
|
41753
|
+
throttle.force({
|
|
41754
|
+
kind: "table-done",
|
|
41755
|
+
table: name,
|
|
41756
|
+
entitiesRendered: keys.length,
|
|
41757
|
+
entitiesTotal: keys.length,
|
|
41758
|
+
tableIndex: 0,
|
|
41759
|
+
tableCount: 0,
|
|
41760
|
+
pct: 100
|
|
41761
|
+
});
|
|
41697
41762
|
}
|
|
41698
41763
|
const entityContextManifest = await this._renderEntityContexts(
|
|
41699
41764
|
outputDir,
|
|
41700
41765
|
filesWritten,
|
|
41701
|
-
counters
|
|
41766
|
+
counters,
|
|
41767
|
+
throttle,
|
|
41768
|
+
signal
|
|
41702
41769
|
);
|
|
41770
|
+
if (entityContextManifest === null) {
|
|
41771
|
+
return this._abortedResult(filesWritten, counters, start);
|
|
41772
|
+
}
|
|
41703
41773
|
if (this._schema.getEntityContexts().size > 0) {
|
|
41704
41774
|
writeManifest(outputDir, {
|
|
41705
41775
|
version: 2,
|
|
@@ -41707,6 +41777,29 @@ var RenderEngine = class {
|
|
|
41707
41777
|
entityContexts: entityContextManifest
|
|
41708
41778
|
});
|
|
41709
41779
|
}
|
|
41780
|
+
const result = {
|
|
41781
|
+
filesWritten,
|
|
41782
|
+
filesSkipped: counters.skipped,
|
|
41783
|
+
durationMs: Date.now() - start
|
|
41784
|
+
};
|
|
41785
|
+
throttle.force({
|
|
41786
|
+
kind: "done",
|
|
41787
|
+
table: null,
|
|
41788
|
+
entitiesRendered: 0,
|
|
41789
|
+
entitiesTotal: 0,
|
|
41790
|
+
tableIndex: 0,
|
|
41791
|
+
tableCount: 0,
|
|
41792
|
+
pct: 100,
|
|
41793
|
+
durationMs: result.durationMs
|
|
41794
|
+
});
|
|
41795
|
+
return result;
|
|
41796
|
+
}
|
|
41797
|
+
/**
|
|
41798
|
+
* Build the partial RenderResult to return when a render is aborted. No
|
|
41799
|
+
* `done` event is emitted — the caller treats abort as "discard the partial
|
|
41800
|
+
* tree", not as a successful completion.
|
|
41801
|
+
*/
|
|
41802
|
+
_abortedResult(filesWritten, counters, start) {
|
|
41710
41803
|
return {
|
|
41711
41804
|
filesWritten,
|
|
41712
41805
|
filesSkipped: counters.skipped,
|
|
@@ -41742,18 +41835,35 @@ var RenderEngine = class {
|
|
|
41742
41835
|
/**
|
|
41743
41836
|
* Render all entity context definitions.
|
|
41744
41837
|
* Mutates `filesWritten` and `counters` in place.
|
|
41745
|
-
* Returns manifest data for the entity contexts rendered this cycle
|
|
41838
|
+
* Returns manifest data for the entity contexts rendered this cycle, or
|
|
41839
|
+
* `null` if the render was aborted mid-flight (the caller discards the
|
|
41840
|
+
* partial tree). Progress is reported through `throttle`; abort is observed
|
|
41841
|
+
* via `signal`.
|
|
41746
41842
|
*/
|
|
41747
|
-
async _renderEntityContexts(outputDir, filesWritten, counters) {
|
|
41843
|
+
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal) {
|
|
41748
41844
|
const manifestData = {};
|
|
41749
41845
|
const protectedTables = /* @__PURE__ */ new Set();
|
|
41750
41846
|
for (const [t8, d6] of this._schema.getEntityContexts()) {
|
|
41751
41847
|
if (d6.protected) protectedTables.add(t8);
|
|
41752
41848
|
}
|
|
41753
|
-
|
|
41849
|
+
const entityTables = [...this._schema.getEntityContexts()];
|
|
41850
|
+
const tableCount = entityTables.length;
|
|
41851
|
+
for (let tableIndex = 0; tableIndex < tableCount; tableIndex++) {
|
|
41852
|
+
if (signal?.aborted) return null;
|
|
41853
|
+
const [table, def] = entityTables[tableIndex];
|
|
41754
41854
|
const entityPk = this._schema.getPrimaryKey(table)[0] ?? "id";
|
|
41755
41855
|
const allRows = await this._schema.queryTable(this._adapter, table);
|
|
41756
41856
|
const directoryRoot = def.directoryRoot ?? table;
|
|
41857
|
+
const entitiesTotal = allRows.length;
|
|
41858
|
+
throttle.force({
|
|
41859
|
+
kind: "table-start",
|
|
41860
|
+
table,
|
|
41861
|
+
entitiesRendered: 0,
|
|
41862
|
+
entitiesTotal,
|
|
41863
|
+
tableIndex,
|
|
41864
|
+
tableCount,
|
|
41865
|
+
pct: 0
|
|
41866
|
+
});
|
|
41757
41867
|
const manifestEntry = {
|
|
41758
41868
|
directoryRoot,
|
|
41759
41869
|
...def.index ? { indexFile: def.index.outputFile } : {},
|
|
@@ -41769,7 +41879,12 @@ var RenderEngine = class {
|
|
|
41769
41879
|
counters.skipped++;
|
|
41770
41880
|
}
|
|
41771
41881
|
}
|
|
41772
|
-
for (
|
|
41882
|
+
for (let i6 = 0; i6 < allRows.length; i6++) {
|
|
41883
|
+
const entityRow = allRows[i6];
|
|
41884
|
+
if (signal?.aborted) return null;
|
|
41885
|
+
if (i6 > 0 && i6 % YIELD_EVERY_ENTITIES === 0) {
|
|
41886
|
+
await new Promise((r6) => setImmediate(r6));
|
|
41887
|
+
}
|
|
41773
41888
|
const rawSlug = def.slug(entityRow);
|
|
41774
41889
|
const slug = rawSlug.replace(/[\u00A0\u2000-\u200B\u202F\u205F\u3000]/g, " ").replace(/[\u0000-\u001F\u007F]/g, "");
|
|
41775
41890
|
if (/[^a-zA-Z0-9.\-_ @(),#&'+:;!~[\]]/.test(slug)) {
|
|
@@ -41814,6 +41929,7 @@ var RenderEngine = class {
|
|
|
41814
41929
|
const entityFileHashes = {};
|
|
41815
41930
|
const protection = protectedTables.size > 0 ? { protectedTables, currentTable: table } : void 0;
|
|
41816
41931
|
for (const [filename, spec] of Object.entries(def.files)) {
|
|
41932
|
+
if (signal?.aborted) return null;
|
|
41817
41933
|
const mergeDefaults = def.sourceDefaults && spec.source.type !== "self" && spec.source.type !== "custom" && spec.source.type !== "enriched";
|
|
41818
41934
|
const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
|
|
41819
41935
|
const rows = await resolveEntitySource(
|
|
@@ -41861,8 +41977,27 @@ var RenderEngine = class {
|
|
|
41861
41977
|
}
|
|
41862
41978
|
}
|
|
41863
41979
|
manifestEntry.entities[slug] = entityFileHashes;
|
|
41980
|
+
const entitiesRendered = i6 + 1;
|
|
41981
|
+
throttle.tick({
|
|
41982
|
+
kind: "table-progress",
|
|
41983
|
+
table,
|
|
41984
|
+
entitiesRendered,
|
|
41985
|
+
entitiesTotal,
|
|
41986
|
+
tableIndex,
|
|
41987
|
+
tableCount,
|
|
41988
|
+
pct: entitiesTotal > 0 ? entitiesRendered / entitiesTotal * 100 : 100
|
|
41989
|
+
});
|
|
41864
41990
|
}
|
|
41865
41991
|
manifestData[table] = manifestEntry;
|
|
41992
|
+
throttle.force({
|
|
41993
|
+
kind: "table-done",
|
|
41994
|
+
table,
|
|
41995
|
+
entitiesRendered: entitiesTotal,
|
|
41996
|
+
entitiesTotal,
|
|
41997
|
+
tableIndex,
|
|
41998
|
+
tableCount,
|
|
41999
|
+
pct: 100
|
|
42000
|
+
});
|
|
41866
42001
|
}
|
|
41867
42002
|
return manifestData;
|
|
41868
42003
|
}
|
|
@@ -44461,6 +44596,54 @@ var Lattice = class _Lattice {
|
|
|
44461
44596
|
async insert(table, row, provenance) {
|
|
44462
44597
|
const notInit = this._notInitError();
|
|
44463
44598
|
if (notInit) return notInit;
|
|
44599
|
+
const { sql, values, pkValue, rowWithPk } = this._prepareInsert(table, row);
|
|
44600
|
+
await runAsyncOrSync(this._adapter, sql, values);
|
|
44601
|
+
await this._afterInsert(table, pkValue, rowWithPk, provenance);
|
|
44602
|
+
return pkValue;
|
|
44603
|
+
}
|
|
44604
|
+
/**
|
|
44605
|
+
* Insert a row while atomically forcing its cloud row-visibility, regardless of
|
|
44606
|
+
* the table's `default_row_visibility`. The per-table insert trigger reads a
|
|
44607
|
+
* transaction-local GUC (`lattice.force_row_visibility`); we set it and run the
|
|
44608
|
+
* INSERT inside a single transaction, so the row is stamped at `visibility` the
|
|
44609
|
+
* instant it exists — it is never momentarily visible at the table default, and
|
|
44610
|
+
* the change-feed `NOTIFY` (delivered only at COMMIT) fires when the row already
|
|
44611
|
+
* carries this visibility. This closes the create-then-demote window that a
|
|
44612
|
+
* plain `insert()` + `setRowVisibility()` would leave open.
|
|
44613
|
+
*
|
|
44614
|
+
* Postgres-only: SQLite is single-user (no cross-viewer leak) and has no trigger
|
|
44615
|
+
* to read the GUC, so it degrades to a plain {@link insert}. A `never_share`
|
|
44616
|
+
* table still wins — its rows are forced private even if `visibility` is
|
|
44617
|
+
* `'everyone'` (the trigger enforces that precedence).
|
|
44618
|
+
*
|
|
44619
|
+
* @since 3.1.0
|
|
44620
|
+
*/
|
|
44621
|
+
async insertForcingVisibility(table, row, visibility, provenance) {
|
|
44622
|
+
const notInit = this._notInitError();
|
|
44623
|
+
if (notInit) return notInit;
|
|
44624
|
+
const vis = visibility;
|
|
44625
|
+
if (vis !== "private" && vis !== "everyone") {
|
|
44626
|
+
throw new Error(`lattice: invalid forced visibility "${vis}"`);
|
|
44627
|
+
}
|
|
44628
|
+
const withClient = this._adapter.withClient?.bind(this._adapter);
|
|
44629
|
+
if (this.getDialect() !== "postgres" || !withClient) {
|
|
44630
|
+
return this.insert(table, row, provenance);
|
|
44631
|
+
}
|
|
44632
|
+
const { sql, values, pkValue, rowWithPk } = this._prepareInsert(table, row);
|
|
44633
|
+
await withClient(async (tx) => {
|
|
44634
|
+
await tx.run(`SELECT set_config('lattice.force_row_visibility', ?, true)`, [visibility]);
|
|
44635
|
+
await tx.run(sql, values);
|
|
44636
|
+
});
|
|
44637
|
+
await this._afterInsert(table, pkValue, rowWithPk, provenance);
|
|
44638
|
+
return pkValue;
|
|
44639
|
+
}
|
|
44640
|
+
/**
|
|
44641
|
+
* Build the INSERT statement + canonical pk for a row (sanitize → schema-filter →
|
|
44642
|
+
* auto-pk → encrypt). Shared by {@link insert} and {@link insertForcingVisibility}
|
|
44643
|
+
* so both produce byte-identical writes; the latter only differs in running it
|
|
44644
|
+
* inside a GUC-scoped transaction.
|
|
44645
|
+
*/
|
|
44646
|
+
_prepareInsert(table, row) {
|
|
44464
44647
|
this._assertIdent(table);
|
|
44465
44648
|
const sanitized = this._filterToSchemaColumns(table, this._sanitizer.sanitizeRow(row));
|
|
44466
44649
|
const pkCols = this._schema.getPrimaryKey(table);
|
|
@@ -44476,12 +44659,17 @@ var Lattice = class _Lattice {
|
|
|
44476
44659
|
const cols = Object.keys(encrypted).map((c6) => `"${c6}"`).join(", ");
|
|
44477
44660
|
const placeholders = Object.keys(encrypted).map(() => "?").join(", ");
|
|
44478
44661
|
const values = Object.values(encrypted);
|
|
44479
|
-
await runAsyncOrSync(
|
|
44480
|
-
this._adapter,
|
|
44481
|
-
`INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`,
|
|
44482
|
-
values
|
|
44483
|
-
);
|
|
44484
44662
|
const pkValue = this._serializeRowPk(table, rowWithPk);
|
|
44663
|
+
return {
|
|
44664
|
+
sql: `INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`,
|
|
44665
|
+
values,
|
|
44666
|
+
pkValue,
|
|
44667
|
+
rowWithPk
|
|
44668
|
+
};
|
|
44669
|
+
}
|
|
44670
|
+
/** Post-insert side effects (changelog, audit, write hooks, embedding sync),
|
|
44671
|
+
* identical for the plain and force-visibility insert paths. */
|
|
44672
|
+
async _afterInsert(table, pkValue, rowWithPk, provenance) {
|
|
44485
44673
|
await this._appendChangelog(
|
|
44486
44674
|
table,
|
|
44487
44675
|
pkValue,
|
|
@@ -44495,7 +44683,6 @@ var Lattice = class _Lattice {
|
|
|
44495
44683
|
this._sanitizer.emitAudit(table, "insert", pkValue);
|
|
44496
44684
|
await this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
|
|
44497
44685
|
this._syncEmbedding(table, "insert", rowWithPk, pkValue);
|
|
44498
|
-
return pkValue;
|
|
44499
44686
|
}
|
|
44500
44687
|
/**
|
|
44501
44688
|
* Insert a row and return the full inserted row (including auto-generated
|
|
@@ -44562,6 +44749,7 @@ var Lattice = class _Lattice {
|
|
|
44562
44749
|
const sanitized = this._filterToSchemaColumns(table, this._sanitizer.sanitizeRow(row));
|
|
44563
44750
|
const encrypted = this._encryptRow(table, sanitized);
|
|
44564
44751
|
const setCols = Object.keys(encrypted).map((c6) => `"${c6}" = ?`).join(", ");
|
|
44752
|
+
if (setCols === "") return;
|
|
44565
44753
|
const { clause, params: pkParams } = this._pkWhere(table, id);
|
|
44566
44754
|
let previousValues = null;
|
|
44567
44755
|
if (this._changelogTables.has(table)) {
|
|
@@ -45178,13 +45366,32 @@ var Lattice = class _Lattice {
|
|
|
45178
45366
|
// -------------------------------------------------------------------------
|
|
45179
45367
|
// Sync
|
|
45180
45368
|
// -------------------------------------------------------------------------
|
|
45181
|
-
async render(outputDir) {
|
|
45369
|
+
async render(outputDir, opts = {}) {
|
|
45182
45370
|
const notInit = this._notInitError();
|
|
45183
45371
|
if (notInit) return notInit;
|
|
45184
|
-
const result = await this._render.render(outputDir);
|
|
45372
|
+
const result = await this._render.render(outputDir, opts);
|
|
45185
45373
|
for (const h6 of this._renderHandlers) h6(result);
|
|
45186
45374
|
return result;
|
|
45187
45375
|
}
|
|
45376
|
+
/**
|
|
45377
|
+
* Render into `outputDir` through the shared single-flight guard, intended to
|
|
45378
|
+
* be called fire-and-forget (e.g. the GUI's instant-open background render).
|
|
45379
|
+
*
|
|
45380
|
+
* The guard ({@link _renderGuarded}) holds {@link _autoRenderInFlight} for the
|
|
45381
|
+
* render's duration, so a data mutation that lands while this render is in
|
|
45382
|
+
* flight is deferred by {@link _runAutoRender} and coalesced — when this
|
|
45383
|
+
* render settles, `finally` clears the flag and re-arms exactly one follow-up
|
|
45384
|
+
* render via {@link _rearmAutoRenderIfPending}. Net invariant: at most one
|
|
45385
|
+
* render to a given dir at a time.
|
|
45386
|
+
*
|
|
45387
|
+
* Errors propagate to the caller (the GUI surfaces them, never silently swallowed); they are
|
|
45388
|
+
* not swallowed here.
|
|
45389
|
+
*/
|
|
45390
|
+
async renderInBackground(outputDir, opts = {}) {
|
|
45391
|
+
const notInit = this._notInitError();
|
|
45392
|
+
if (notInit) return notInit;
|
|
45393
|
+
return this._renderGuarded(outputDir, opts);
|
|
45394
|
+
}
|
|
45188
45395
|
async sync(outputDir) {
|
|
45189
45396
|
const notInit = this._notInitError();
|
|
45190
45397
|
if (notInit) return notInit;
|
|
@@ -45526,6 +45733,30 @@ var Lattice = class _Lattice {
|
|
|
45526
45733
|
}, this._autoRenderDebounceMs);
|
|
45527
45734
|
this._autoRenderTimer.unref();
|
|
45528
45735
|
}
|
|
45736
|
+
/**
|
|
45737
|
+
* Shared single-flight render path used by {@link renderInBackground}.
|
|
45738
|
+
*
|
|
45739
|
+
* Holds {@link _autoRenderInFlight} for the render's duration so the
|
|
45740
|
+
* mutation-driven {@link _runAutoRender} defers while this render runs (it
|
|
45741
|
+
* sees the flag and marks itself pending instead of starting a second,
|
|
45742
|
+
* overlapping render). On settle, `finally` clears the flag and re-arms a
|
|
45743
|
+
* single coalesced follow-up render if any mutation arrived mid-flight.
|
|
45744
|
+
* Errors propagate to the caller; the flag is always cleared.
|
|
45745
|
+
*/
|
|
45746
|
+
async _renderGuarded(outputDir, opts) {
|
|
45747
|
+
while (this._autoRenderInFlight) {
|
|
45748
|
+
await new Promise((r6) => setImmediate(r6));
|
|
45749
|
+
}
|
|
45750
|
+
this._autoRenderInFlight = true;
|
|
45751
|
+
try {
|
|
45752
|
+
const result = await this._render.render(outputDir, opts);
|
|
45753
|
+
for (const h6 of this._renderHandlers) h6(result);
|
|
45754
|
+
return result;
|
|
45755
|
+
} finally {
|
|
45756
|
+
this._autoRenderInFlight = false;
|
|
45757
|
+
this._rearmAutoRenderIfPending();
|
|
45758
|
+
}
|
|
45759
|
+
}
|
|
45529
45760
|
async _runAutoRender() {
|
|
45530
45761
|
const dir = this._autoRenderDir;
|
|
45531
45762
|
if (!dir || !this._initialized) return;
|
|
@@ -47153,6 +47384,31 @@ function pkSqlExpr(pkCols, prefix) {
|
|
|
47153
47384
|
return pkCols.map((c6) => `CAST(${prefix}"${c6}" AS TEXT)`).join(` || chr(9) || `);
|
|
47154
47385
|
}
|
|
47155
47386
|
var MEMBER_GROUP = "lattice_members";
|
|
47387
|
+
function pinDefinerSearchPath(sql, schema) {
|
|
47388
|
+
const safe = schema.replace(/"/g, '""');
|
|
47389
|
+
return sql.replace(
|
|
47390
|
+
/SECURITY DEFINER AS/g,
|
|
47391
|
+
`SECURITY DEFINER SET search_path = "${safe}", pg_temp AS`
|
|
47392
|
+
);
|
|
47393
|
+
}
|
|
47394
|
+
async function cloudSchema(db) {
|
|
47395
|
+
const row = await getAsyncOrSync(db.adapter, `SELECT current_schema() AS schema`);
|
|
47396
|
+
const s2 = row?.schema;
|
|
47397
|
+
if (typeof s2 !== "string" || s2.length === 0) {
|
|
47398
|
+
throw new Error("cloud RLS: could not resolve current_schema() for search_path pinning");
|
|
47399
|
+
}
|
|
47400
|
+
return s2;
|
|
47401
|
+
}
|
|
47402
|
+
function revokeSchemaCreateSql(schema) {
|
|
47403
|
+
const lit = `'${schema.replace(/'/g, "''")}'`;
|
|
47404
|
+
return `
|
|
47405
|
+
DO $LATTICE_REVOKE$ BEGIN
|
|
47406
|
+
EXECUTE format('REVOKE CREATE ON SCHEMA %I FROM PUBLIC', ${lit});
|
|
47407
|
+
EXCEPTION WHEN OTHERS THEN
|
|
47408
|
+
NULL; -- not the schema owner, or already revoked
|
|
47409
|
+
END $LATTICE_REVOKE$;
|
|
47410
|
+
`;
|
|
47411
|
+
}
|
|
47156
47412
|
var CLOUD_RLS_BOOTSTRAP_SQL = `
|
|
47157
47413
|
-- Member group (NOLOGIN). Members inherit schema/connect/table privileges from it;
|
|
47158
47414
|
-- RLS filters per the individual member's login role, so the group never widens
|
|
@@ -47212,6 +47468,52 @@ CREATE TABLE IF NOT EXISTS "__lattice_cell_grants" (
|
|
|
47212
47468
|
PRIMARY KEY ("table_name", "pk", "column_name", "grantee_role")
|
|
47213
47469
|
);
|
|
47214
47470
|
|
|
47471
|
+
-- Per-table policy: the owner-controlled defaults that govern a whole table.
|
|
47472
|
+
-- default_row_visibility is the visibility NEW rows are stamped with (the insert
|
|
47473
|
+
-- trigger reads it); never_share is a hard exclusion \u2014 the share/grant functions
|
|
47474
|
+
-- refuse to elevate such a table and the trigger forces its rows private. Owner-
|
|
47475
|
+
-- managed; members have no grant (it never appears in their data API).
|
|
47476
|
+
CREATE TABLE IF NOT EXISTS "__lattice_table_policy" (
|
|
47477
|
+
"table_name" text PRIMARY KEY,
|
|
47478
|
+
"default_row_visibility" text NOT NULL DEFAULT 'private'
|
|
47479
|
+
CHECK ("default_row_visibility" IN ('private','everyone')),
|
|
47480
|
+
"never_share" boolean NOT NULL DEFAULT false,
|
|
47481
|
+
"updated_by" text NOT NULL DEFAULT session_user,
|
|
47482
|
+
"updated_at" timestamptz NOT NULL DEFAULT now()
|
|
47483
|
+
);
|
|
47484
|
+
|
|
47485
|
+
-- Per-column audience policy: the CANONICAL store of which column carries which
|
|
47486
|
+
-- audience spec (role: / subject: / source: / owner / everyone). Previously the
|
|
47487
|
+
-- spec lived only in the owner's on-disk YAML and was compiled into the mask view
|
|
47488
|
+
-- once at init; storing it here makes it cloud-canonical and member-consistent.
|
|
47489
|
+
-- The generated <table>_v mask view is regenerated from THIS table on change.
|
|
47490
|
+
-- Owner-managed; members have no grant.
|
|
47491
|
+
CREATE TABLE IF NOT EXISTS "__lattice_column_policy" (
|
|
47492
|
+
"table_name" text NOT NULL,
|
|
47493
|
+
"column_name" text NOT NULL,
|
|
47494
|
+
"audience" text NOT NULL,
|
|
47495
|
+
"updated_by" text NOT NULL DEFAULT session_user,
|
|
47496
|
+
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
|
47497
|
+
PRIMARY KEY ("table_name", "column_name")
|
|
47498
|
+
);
|
|
47499
|
+
|
|
47500
|
+
-- Owner-only audit of issued member invites: which scoped role was minted for
|
|
47501
|
+
-- which email (HASHED \u2014 the plaintext email is never stored), when it expires,
|
|
47502
|
+
-- and whether it was redeemed/revoked. No plaintext password is ever stored
|
|
47503
|
+
-- (the credential lives only inside the email-bound token the owner delivers).
|
|
47504
|
+
-- Owner-managed; members have no grant. Named distinctly from any legacy
|
|
47505
|
+
-- team-model invitations table so a pre-existing cloud never collides.
|
|
47506
|
+
CREATE TABLE IF NOT EXISTS "__lattice_member_invites" (
|
|
47507
|
+
"id" text PRIMARY KEY,
|
|
47508
|
+
"role" text NOT NULL,
|
|
47509
|
+
"email_hash" text NOT NULL,
|
|
47510
|
+
"created_by" text NOT NULL DEFAULT session_user,
|
|
47511
|
+
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
47512
|
+
"expires_at" timestamptz NOT NULL,
|
|
47513
|
+
"redeemed_at" timestamptz,
|
|
47514
|
+
"revoked_at" timestamptz
|
|
47515
|
+
);
|
|
47516
|
+
|
|
47215
47517
|
-- Visibility check. SECURITY DEFINER so it reads bookkeeping the member can't;
|
|
47216
47518
|
-- keyed on session_user (the member's login role). A row with no ownership record
|
|
47217
47519
|
-- is visible to nobody.
|
|
@@ -47237,6 +47539,10 @@ BEGIN
|
|
|
47237
47539
|
IF p_visibility NOT IN ('private','everyone','custom') THEN
|
|
47238
47540
|
RAISE EXCEPTION 'lattice: invalid visibility %', p_visibility;
|
|
47239
47541
|
END IF;
|
|
47542
|
+
IF p_visibility <> 'private'
|
|
47543
|
+
AND COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = p_table), false) THEN
|
|
47544
|
+
RAISE EXCEPTION 'lattice: "%" is a private-only table and cannot be shared', p_table;
|
|
47545
|
+
END IF;
|
|
47240
47546
|
SELECT o."owner_role" INTO v_owner FROM "__lattice_owners" o
|
|
47241
47547
|
WHERE o."table_name" = p_table AND o."pk" = p_pk;
|
|
47242
47548
|
IF v_owner IS NULL THEN RAISE EXCEPTION 'lattice: no ownership record for %/%', p_table, p_pk; END IF;
|
|
@@ -47250,6 +47556,9 @@ CREATE OR REPLACE FUNCTION lattice_grant_row(p_table text, p_pk text, p_grantee
|
|
|
47250
47556
|
RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
|
|
47251
47557
|
DECLARE v_owner text;
|
|
47252
47558
|
BEGIN
|
|
47559
|
+
IF COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = p_table), false) THEN
|
|
47560
|
+
RAISE EXCEPTION 'lattice: "%" is a private-only table and cannot be shared', p_table;
|
|
47561
|
+
END IF;
|
|
47253
47562
|
SELECT o."owner_role" INTO v_owner FROM "__lattice_owners" o
|
|
47254
47563
|
WHERE o."table_name" = p_table AND o."pk" = p_pk;
|
|
47255
47564
|
IF v_owner IS NULL THEN RAISE EXCEPTION 'lattice: no ownership record for %/%', p_table, p_pk; END IF;
|
|
@@ -47339,6 +47648,9 @@ CREATE OR REPLACE FUNCTION lattice_grant_cell(p_table text, p_pk text, p_column
|
|
|
47339
47648
|
RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
|
|
47340
47649
|
DECLARE v_owner text;
|
|
47341
47650
|
BEGIN
|
|
47651
|
+
IF COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = p_table), false) THEN
|
|
47652
|
+
RAISE EXCEPTION 'lattice: "%" is a private-only table and cannot be shared', p_table;
|
|
47653
|
+
END IF;
|
|
47342
47654
|
SELECT o."owner_role" INTO v_owner FROM "__lattice_owners" o
|
|
47343
47655
|
WHERE o."table_name" = p_table AND o."pk" = p_pk;
|
|
47344
47656
|
IF v_owner IS NULL OR v_owner <> session_user THEN
|
|
@@ -47371,6 +47683,87 @@ RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
|
|
|
47371
47683
|
SELECT lattice_row_visible('files', p_source_ref)
|
|
47372
47684
|
$fn$;
|
|
47373
47685
|
|
|
47686
|
+
-- Is the connected member the OWNER of this row? Used by the "owner" column
|
|
47687
|
+
-- audience (a secret column reveals only to the row owner). SECURITY DEFINER +
|
|
47688
|
+
-- session_user, like the other predicates.
|
|
47689
|
+
CREATE OR REPLACE FUNCTION lattice_is_owner(p_table text, p_pk text)
|
|
47690
|
+
RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
|
|
47691
|
+
SELECT EXISTS (
|
|
47692
|
+
SELECT 1 FROM "__lattice_owners" o
|
|
47693
|
+
WHERE o."table_name" = p_table AND o."pk" = p_pk AND o."owner_role" = session_user
|
|
47694
|
+
)
|
|
47695
|
+
$fn$;
|
|
47696
|
+
|
|
47697
|
+
-- Owner-only: set a table's default row visibility for NEW rows. Raises unless the
|
|
47698
|
+
-- caller can create roles (a cloud owner / DBA), like lattice_assign_role. Rejects
|
|
47699
|
+
-- 'everyone' on a never-share table.
|
|
47700
|
+
CREATE OR REPLACE FUNCTION lattice_set_table_default_visibility(p_table text, p_visibility text)
|
|
47701
|
+
RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
|
|
47702
|
+
BEGIN
|
|
47703
|
+
IF NOT (SELECT rolcreaterole FROM pg_roles WHERE rolname = session_user) THEN
|
|
47704
|
+
RAISE EXCEPTION 'lattice: only a cloud owner may set a table''s default visibility';
|
|
47705
|
+
END IF;
|
|
47706
|
+
IF p_visibility NOT IN ('private','everyone') THEN
|
|
47707
|
+
RAISE EXCEPTION 'lattice: invalid default visibility %', p_visibility;
|
|
47708
|
+
END IF;
|
|
47709
|
+
IF p_visibility = 'everyone'
|
|
47710
|
+
AND COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = p_table), false) THEN
|
|
47711
|
+
RAISE EXCEPTION 'lattice: "%" is a private-only table; its rows cannot default to everyone', p_table;
|
|
47712
|
+
END IF;
|
|
47713
|
+
INSERT INTO "__lattice_table_policy" ("table_name","default_row_visibility","updated_by","updated_at")
|
|
47714
|
+
VALUES (p_table, p_visibility, session_user, now())
|
|
47715
|
+
ON CONFLICT ("table_name") DO UPDATE
|
|
47716
|
+
SET "default_row_visibility" = EXCLUDED."default_row_visibility",
|
|
47717
|
+
"updated_by" = session_user, "updated_at" = now();
|
|
47718
|
+
END $fn$;
|
|
47719
|
+
|
|
47720
|
+
-- Owner-only: mark a table never-shareable (Secrets/Messages-class). When true the
|
|
47721
|
+
-- share/grant functions raise and the insert trigger forces new rows private; the
|
|
47722
|
+
-- default visibility is also forced private. Turning it ON also RETROACTIVELY
|
|
47723
|
+
-- privatizes the table: any row currently shared ('everyone'/'custom') is reset to
|
|
47724
|
+
-- 'private' and every existing row/cell grant on the table is dropped \u2014 otherwise
|
|
47725
|
+
-- flagging a table never-share would leave already-leaked rows visible, defeating
|
|
47726
|
+
-- the point. Idempotent: re-running with already-private rows updates nothing.
|
|
47727
|
+
CREATE OR REPLACE FUNCTION lattice_set_table_never_share(p_table text, p_on boolean)
|
|
47728
|
+
RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
|
|
47729
|
+
BEGIN
|
|
47730
|
+
IF NOT (SELECT rolcreaterole FROM pg_roles WHERE rolname = session_user) THEN
|
|
47731
|
+
RAISE EXCEPTION 'lattice: only a cloud owner may change a table''s never-share flag';
|
|
47732
|
+
END IF;
|
|
47733
|
+
INSERT INTO "__lattice_table_policy" ("table_name","never_share","default_row_visibility","updated_by","updated_at")
|
|
47734
|
+
VALUES (p_table, p_on, CASE WHEN p_on THEN 'private' ELSE 'private' END, session_user, now())
|
|
47735
|
+
ON CONFLICT ("table_name") DO UPDATE
|
|
47736
|
+
SET "never_share" = EXCLUDED."never_share",
|
|
47737
|
+
"default_row_visibility" = CASE WHEN EXCLUDED."never_share"
|
|
47738
|
+
THEN 'private' ELSE "__lattice_table_policy"."default_row_visibility" END,
|
|
47739
|
+
"updated_by" = session_user, "updated_at" = now();
|
|
47740
|
+
IF p_on THEN
|
|
47741
|
+
UPDATE "__lattice_owners" SET "visibility" = 'private', "updated_at" = now()
|
|
47742
|
+
WHERE "table_name" = p_table AND "visibility" <> 'private';
|
|
47743
|
+
DELETE FROM "__lattice_row_grants" WHERE "table_name" = p_table;
|
|
47744
|
+
DELETE FROM "__lattice_cell_grants" WHERE "table_name" = p_table;
|
|
47745
|
+
END IF;
|
|
47746
|
+
END $fn$;
|
|
47747
|
+
|
|
47748
|
+
-- Owner-only: set (or clear) a column's audience spec in the canonical DB store.
|
|
47749
|
+
-- An empty/null spec removes the policy row (column becomes unmasked). The GUI/lib
|
|
47750
|
+
-- regenerates the table's mask view from this store after calling this.
|
|
47751
|
+
CREATE OR REPLACE FUNCTION lattice_set_column_audience(p_table text, p_column text, p_audience text)
|
|
47752
|
+
RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
|
|
47753
|
+
BEGIN
|
|
47754
|
+
IF NOT (SELECT rolcreaterole FROM pg_roles WHERE rolname = session_user) THEN
|
|
47755
|
+
RAISE EXCEPTION 'lattice: only a cloud owner may set a column audience';
|
|
47756
|
+
END IF;
|
|
47757
|
+
IF p_audience IS NULL OR btrim(p_audience) = '' THEN
|
|
47758
|
+
DELETE FROM "__lattice_column_policy" WHERE "table_name" = p_table AND "column_name" = p_column;
|
|
47759
|
+
ELSE
|
|
47760
|
+
INSERT INTO "__lattice_column_policy" ("table_name","column_name","audience","updated_by","updated_at")
|
|
47761
|
+
VALUES (p_table, p_column, p_audience, session_user, now())
|
|
47762
|
+
ON CONFLICT ("table_name","column_name") DO UPDATE
|
|
47763
|
+
SET "audience" = EXCLUDED."audience", "updated_by" = session_user, "updated_at" = now();
|
|
47764
|
+
END IF;
|
|
47765
|
+
END $fn$;
|
|
47766
|
+
|
|
47374
47767
|
-- Append-only change feed. The per-table ownership trigger records one row per
|
|
47375
47768
|
-- INSERT/UPDATE/DELETE; the AFTER INSERT trigger here fires pg_notify so a
|
|
47376
47769
|
-- connected member's realtime broker refreshes. Members get no direct access \u2014
|
|
@@ -47415,7 +47808,20 @@ CREATE OR REPLACE FUNCTION "${trg}"() RETURNS trigger LANGUAGE plpgsql SECURITY
|
|
|
47415
47808
|
BEGIN
|
|
47416
47809
|
IF TG_OP = 'INSERT' THEN
|
|
47417
47810
|
INSERT INTO "__lattice_owners" ("table_name","pk","owner_role","visibility")
|
|
47418
|
-
VALUES (${lit}, ${pkNew}, session_user,
|
|
47811
|
+
VALUES (${lit}, ${pkNew}, session_user,
|
|
47812
|
+
CASE
|
|
47813
|
+
-- never-share always wins: such a table's rows are private, full stop.
|
|
47814
|
+
WHEN COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = ${lit}), false)
|
|
47815
|
+
THEN 'private'
|
|
47816
|
+
-- per-INSERT override: a caller forcing visibility for THIS write (e.g.
|
|
47817
|
+
-- chat "private mode") sets the transaction-local lattice.force_row_visibility
|
|
47818
|
+
-- GUC, so the row is stamped atomically at insert \u2014 never momentarily at
|
|
47819
|
+
-- the table default, and the change-feed NOTIFY (deferred to COMMIT) only
|
|
47820
|
+
-- fires once the row already carries this visibility.
|
|
47821
|
+
WHEN NULLIF(current_setting('lattice.force_row_visibility', true), '') IN ('private','everyone')
|
|
47822
|
+
THEN current_setting('lattice.force_row_visibility', true)
|
|
47823
|
+
ELSE COALESCE((SELECT "default_row_visibility" FROM "__lattice_table_policy" WHERE "table_name" = ${lit}), 'private')
|
|
47824
|
+
END)
|
|
47419
47825
|
ON CONFLICT ("table_name","pk") DO NOTHING;
|
|
47420
47826
|
INSERT INTO "__lattice_changes" ("table_name","pk","op","owner_role")
|
|
47421
47827
|
VALUES (${lit}, ${pkNew}, 'upsert', session_user);
|
|
@@ -47455,19 +47861,28 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
|
|
|
47455
47861
|
}
|
|
47456
47862
|
async function installCloudRls(db) {
|
|
47457
47863
|
if (!isPg(db)) return;
|
|
47864
|
+
const schema = await cloudSchema(db);
|
|
47458
47865
|
const migration = {
|
|
47459
47866
|
// v3 added the audience helpers; v4 the role model; v5 the per-card override
|
|
47460
|
-
// model (__lattice_cell_grants + lattice_cell_visible / lattice_grant_cell)
|
|
47461
|
-
//
|
|
47462
|
-
|
|
47463
|
-
|
|
47867
|
+
// model (__lattice_cell_grants + lattice_cell_visible / lattice_grant_cell);
|
|
47868
|
+
// v6 added per-table policy (__lattice_table_policy: default_row_visibility +
|
|
47869
|
+
// never_share, enforced in the insert trigger + share/grant guards), the
|
|
47870
|
+
// canonical column-audience store (__lattice_column_policy), lattice_is_owner,
|
|
47871
|
+
// and the owner-only setters; v7 pins search_path on every SECURITY DEFINER
|
|
47872
|
+
// helper (closes the pg_temp-shadow RLS bypass) + revokes schema CREATE from
|
|
47873
|
+
// PUBLIC. The bootstrap is fully idempotent.
|
|
47874
|
+
version: "internal:cloud-rls:bootstrap:v7",
|
|
47875
|
+
sql: pinDefinerSearchPath(CLOUD_RLS_BOOTSTRAP_SQL, schema) + revokeSchemaCreateSql(schema)
|
|
47464
47876
|
};
|
|
47465
47877
|
await db.migrate([migration]);
|
|
47466
47878
|
}
|
|
47467
47879
|
async function enableChangelogRls(db) {
|
|
47468
47880
|
if (!isPg(db)) return;
|
|
47469
47881
|
const migration = {
|
|
47470
|
-
|
|
47882
|
+
// v2: ground-truth/audit entries are owner-only (was lattice_row_visible),
|
|
47883
|
+
// closing the masked-column-via-history leak. Bump re-installs the policy on
|
|
47884
|
+
// existing clouds.
|
|
47885
|
+
version: "internal:cloud-rls:changelog:v2",
|
|
47471
47886
|
sql: `
|
|
47472
47887
|
ALTER TABLE "__lattice_changelog" ENABLE ROW LEVEL SECURITY;
|
|
47473
47888
|
ALTER TABLE "__lattice_changelog" FORCE ROW LEVEL SECURITY;
|
|
@@ -47482,7 +47897,7 @@ CREATE POLICY "lattice_changelog_sel" ON "__lattice_changelog" FOR SELECT USING
|
|
|
47482
47897
|
SELECT 1 FROM jsonb_array_elements_text("source_ref"::jsonb) AS src(sid)
|
|
47483
47898
|
WHERE NOT lattice_source_visible(src.sid)
|
|
47484
47899
|
)
|
|
47485
|
-
ELSE
|
|
47900
|
+
ELSE lattice_is_owner("table_name", "row_id")
|
|
47486
47901
|
END
|
|
47487
47902
|
);
|
|
47488
47903
|
DROP POLICY IF EXISTS "lattice_changelog_ins" ON "__lattice_changelog";
|
|
@@ -47493,9 +47908,10 @@ CREATE POLICY "lattice_changelog_ins" ON "__lattice_changelog" FOR INSERT WITH C
|
|
|
47493
47908
|
}
|
|
47494
47909
|
async function enableRlsForTable(db, table, pkCols) {
|
|
47495
47910
|
if (!isPg(db)) return;
|
|
47911
|
+
const schema = await cloudSchema(db);
|
|
47496
47912
|
const migration = {
|
|
47497
|
-
version: `internal:cloud-rls:table:${table}:
|
|
47498
|
-
sql: tableRlsSql(table, pkCols)
|
|
47913
|
+
version: `internal:cloud-rls:table:${table}:v3`,
|
|
47914
|
+
sql: pinDefinerSearchPath(tableRlsSql(table, pkCols), schema)
|
|
47499
47915
|
};
|
|
47500
47916
|
await db.migrate([migration]);
|
|
47501
47917
|
}
|
|
@@ -47625,12 +48041,17 @@ function isRowAudience(audience) {
|
|
|
47625
48041
|
const a6 = (audience ?? "").trim();
|
|
47626
48042
|
return a6 === "" || a6 === "everyone" || a6 === "row-audience";
|
|
47627
48043
|
}
|
|
47628
|
-
function audiencePredicate(audience) {
|
|
48044
|
+
function audiencePredicate(audience, ctx) {
|
|
47629
48045
|
if (isRowAudience(audience)) return "true";
|
|
47630
48046
|
const clauses = audience.split("+").map((c6) => c6.trim()).filter(Boolean);
|
|
47631
48047
|
const parts = [];
|
|
47632
48048
|
for (const clause of clauses) {
|
|
47633
48049
|
if (clause === "everyone" || clause === "row-audience") return "true";
|
|
48050
|
+
if (clause === "owner") {
|
|
48051
|
+
if (!ctx) throw new Error('lattice: the "owner" audience needs a row context');
|
|
48052
|
+
parts.push(`lattice_is_owner(${ctx.tableLit}, ${ctx.pkExpr})`);
|
|
48053
|
+
continue;
|
|
48054
|
+
}
|
|
47634
48055
|
const idx = clause.indexOf(":");
|
|
47635
48056
|
const kind = idx === -1 ? clause : clause.slice(0, idx);
|
|
47636
48057
|
const arg = idx === -1 ? "" : clause.slice(idx + 1).trim();
|
|
@@ -47665,7 +48086,7 @@ function audienceViewSql(table, columns, pkCols, columnAudience) {
|
|
|
47665
48086
|
const selectCols = columns.map((col) => {
|
|
47666
48087
|
const aud = columnAudience[col] ?? "";
|
|
47667
48088
|
if (isRowAudience(aud)) return quoteIdent(col);
|
|
47668
|
-
const pred = audiencePredicate(aud);
|
|
48089
|
+
const pred = audiencePredicate(aud, { tableLit: lit, pkExpr });
|
|
47669
48090
|
if (pred === "true") return quoteIdent(col);
|
|
47670
48091
|
const colLit = `'${col.replace(/'/g, "''")}'`;
|
|
47671
48092
|
const full = `(${pred}) OR lattice_cell_visible(${lit}, ${pkExpr}, ${colLit})`;
|
|
@@ -47700,6 +48121,92 @@ async function enableAudienceView(db, table, columns, pkCols, columnAudience) {
|
|
|
47700
48121
|
};
|
|
47701
48122
|
await db.migrate([migration]);
|
|
47702
48123
|
}
|
|
48124
|
+
async function loadColumnPolicy(db, table) {
|
|
48125
|
+
if (db.getDialect() !== "postgres") return {};
|
|
48126
|
+
const rows = await allAsyncOrSync(
|
|
48127
|
+
db.adapter,
|
|
48128
|
+
`SELECT "column_name", "audience" FROM "__lattice_column_policy" WHERE "table_name" = ?`,
|
|
48129
|
+
[table]
|
|
48130
|
+
);
|
|
48131
|
+
const out = {};
|
|
48132
|
+
for (const r6 of rows) out[r6.column_name] = r6.audience;
|
|
48133
|
+
return out;
|
|
48134
|
+
}
|
|
48135
|
+
async function seedColumnPolicyFromYaml(db, table, yamlAudience) {
|
|
48136
|
+
if (db.getDialect() !== "postgres") return;
|
|
48137
|
+
const marker = `internal:cloud-column-seed:${table}:v1`;
|
|
48138
|
+
const already = await getAsyncOrSync(
|
|
48139
|
+
db.adapter,
|
|
48140
|
+
`SELECT 1 AS one FROM "__lattice_migrations" WHERE "version" = ?`,
|
|
48141
|
+
[marker]
|
|
48142
|
+
);
|
|
48143
|
+
if (already) return;
|
|
48144
|
+
for (const [col, aud] of Object.entries(yamlAudience)) {
|
|
48145
|
+
if (isRowAudience(aud)) continue;
|
|
48146
|
+
await runAsyncOrSync(
|
|
48147
|
+
db.adapter,
|
|
48148
|
+
`INSERT INTO "__lattice_column_policy" ("table_name","column_name","audience")
|
|
48149
|
+
VALUES (?, ?, ?) ON CONFLICT ("table_name","column_name") DO NOTHING`,
|
|
48150
|
+
[table, col, aud]
|
|
48151
|
+
);
|
|
48152
|
+
}
|
|
48153
|
+
await runAsyncOrSync(
|
|
48154
|
+
db.adapter,
|
|
48155
|
+
`INSERT INTO "__lattice_migrations" ("version","applied_at") VALUES (?, ?)
|
|
48156
|
+
ON CONFLICT ("version") DO NOTHING`,
|
|
48157
|
+
[marker, (/* @__PURE__ */ new Date()).toISOString()]
|
|
48158
|
+
);
|
|
48159
|
+
}
|
|
48160
|
+
async function regenerateAudienceViewFromDb(db, table, columns, pkCols) {
|
|
48161
|
+
if (db.getDialect() !== "postgres") return;
|
|
48162
|
+
if (pkCols.length === 0) return;
|
|
48163
|
+
const spec = await loadColumnPolicy(db, table);
|
|
48164
|
+
const view = quoteIdent(`${table}_v`);
|
|
48165
|
+
const base = quoteIdent(table);
|
|
48166
|
+
if (!tableNeedsAudienceView(spec)) {
|
|
48167
|
+
await runAsyncOrSync(
|
|
48168
|
+
db.adapter,
|
|
48169
|
+
`DROP VIEW IF EXISTS ${view};
|
|
48170
|
+
GRANT SELECT ON ${base} TO ${MEMBER_GROUP};`
|
|
48171
|
+
);
|
|
48172
|
+
return;
|
|
48173
|
+
}
|
|
48174
|
+
await runAsyncOrSync(db.adapter, audienceViewSql(table, columns, pkCols, spec));
|
|
48175
|
+
}
|
|
48176
|
+
async function setColumnAudience(db, table, column, audience, columns, pkCols) {
|
|
48177
|
+
if (db.getDialect() !== "postgres") return;
|
|
48178
|
+
await runAsyncOrSync(db.adapter, `SELECT lattice_set_column_audience(?, ?, ?)`, [
|
|
48179
|
+
table,
|
|
48180
|
+
column,
|
|
48181
|
+
audience
|
|
48182
|
+
]);
|
|
48183
|
+
await regenerateAudienceViewFromDb(db, table, columns, pkCols);
|
|
48184
|
+
}
|
|
48185
|
+
|
|
48186
|
+
// src/cloud/table-policy.ts
|
|
48187
|
+
async function getTablePolicy(db, table) {
|
|
48188
|
+
if (db.getDialect() !== "postgres") return { defaultRowVisibility: "private", neverShare: false };
|
|
48189
|
+
const row = await getAsyncOrSync(
|
|
48190
|
+
db.adapter,
|
|
48191
|
+
`SELECT "default_row_visibility", "never_share" FROM "__lattice_table_policy" WHERE "table_name" = ?`,
|
|
48192
|
+
[table]
|
|
48193
|
+
);
|
|
48194
|
+
return {
|
|
48195
|
+
defaultRowVisibility: row?.default_row_visibility === "everyone" ? "everyone" : "private",
|
|
48196
|
+
neverShare: row?.never_share === true
|
|
48197
|
+
};
|
|
48198
|
+
}
|
|
48199
|
+
async function setTableDefaultVisibility(db, table, visibility) {
|
|
48200
|
+
if (db.getDialect() !== "postgres") return;
|
|
48201
|
+
await runAsyncOrSync(db.adapter, `SELECT lattice_set_table_default_visibility(?, ?)`, [
|
|
48202
|
+
table,
|
|
48203
|
+
visibility
|
|
48204
|
+
]);
|
|
48205
|
+
}
|
|
48206
|
+
async function setTableNeverShare(db, table, on) {
|
|
48207
|
+
if (db.getDialect() !== "postgres") return;
|
|
48208
|
+
await runAsyncOrSync(db.adapter, `SELECT lattice_set_table_never_share(?, ?)`, [table, on]);
|
|
48209
|
+
}
|
|
47703
48210
|
|
|
47704
48211
|
// src/cloud/fold-cache.ts
|
|
47705
48212
|
function viewerSignature(viewer) {
|
|
@@ -47774,9 +48281,12 @@ END $fn$;
|
|
|
47774
48281
|
`;
|
|
47775
48282
|
async function installCloudSettings(db) {
|
|
47776
48283
|
if (db.getDialect() !== "postgres") return;
|
|
48284
|
+
const schema = await cloudSchema(db);
|
|
47777
48285
|
const migration = {
|
|
47778
|
-
|
|
47779
|
-
|
|
48286
|
+
// v2 pins search_path on the two SECURITY DEFINER helpers (closes the
|
|
48287
|
+
// pg_temp-shadow class of bypass on the settings getter/setter).
|
|
48288
|
+
version: "internal:cloud-settings:v2",
|
|
48289
|
+
sql: pinDefinerSearchPath(CLOUD_SETTINGS_BOOTSTRAP_SQL, schema)
|
|
47780
48290
|
};
|
|
47781
48291
|
await db.migrate([migration]);
|
|
47782
48292
|
}
|
|
@@ -47803,7 +48313,8 @@ async function secureCloud(db) {
|
|
|
47803
48313
|
await installCloudSettings(db);
|
|
47804
48314
|
await db.ensureObservationSubstrate();
|
|
47805
48315
|
await enableChangelogRls(db);
|
|
47806
|
-
|
|
48316
|
+
const registered = db.getRegisteredTableNames();
|
|
48317
|
+
for (const table of registered) {
|
|
47807
48318
|
if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) continue;
|
|
47808
48319
|
const pk = db.getPrimaryKey(table);
|
|
47809
48320
|
if (pk.length === 0) continue;
|
|
@@ -47811,9 +48322,21 @@ async function secureCloud(db) {
|
|
|
47811
48322
|
await enableRlsForTable(db, table, pk);
|
|
47812
48323
|
const cols = db.getRegisteredColumns(table);
|
|
47813
48324
|
if (cols) {
|
|
47814
|
-
await
|
|
48325
|
+
await seedColumnPolicyFromYaml(db, table, db.getColumnAudience(table));
|
|
48326
|
+
await regenerateAudienceViewFromDb(db, table, Object.keys(cols), pk);
|
|
47815
48327
|
}
|
|
47816
48328
|
}
|
|
48329
|
+
if (registered.includes("secrets")) {
|
|
48330
|
+
await runAsyncOrSync(db.adapter, `SELECT lattice_set_table_never_share('secrets', true)`);
|
|
48331
|
+
}
|
|
48332
|
+
await runAsyncOrSync(
|
|
48333
|
+
db.adapter,
|
|
48334
|
+
`DO $LATTICE$ BEGIN
|
|
48335
|
+
IF to_regclass('__lattice_user_identity') IS NOT NULL THEN
|
|
48336
|
+
EXECUTE 'GRANT SELECT, INSERT, UPDATE ON "__lattice_user_identity" TO ${MEMBER_GROUP}';
|
|
48337
|
+
END IF;
|
|
48338
|
+
END $LATTICE$`
|
|
48339
|
+
);
|
|
47817
48340
|
}
|
|
47818
48341
|
|
|
47819
48342
|
// src/ai/llm-client.ts
|
|
@@ -48410,6 +48933,7 @@ function defaultPdfSender(auth) {
|
|
|
48410
48933
|
NATIVE_ENTITY_NAMES,
|
|
48411
48934
|
NATIVE_REGISTRY_TABLE,
|
|
48412
48935
|
PostgresAdapter,
|
|
48936
|
+
ProgressThrottle,
|
|
48413
48937
|
READ_ONLY_HEADER,
|
|
48414
48938
|
ROOT_DIRNAME,
|
|
48415
48939
|
ReferenceUnavailableError,
|
|
@@ -48473,6 +48997,7 @@ function defaultPdfSender(auth) {
|
|
|
48473
48997
|
getCloudSetting,
|
|
48474
48998
|
getDbCredential,
|
|
48475
48999
|
getOrCreateMasterKey,
|
|
49000
|
+
getTablePolicy,
|
|
48476
49001
|
getWorkspace,
|
|
48477
49002
|
grantCell,
|
|
48478
49003
|
hasFtsIndex,
|
|
@@ -48490,6 +49015,7 @@ function defaultPdfSender(auth) {
|
|
|
48490
49015
|
listNativeBindings,
|
|
48491
49016
|
listTokens,
|
|
48492
49017
|
listWorkspaces,
|
|
49018
|
+
loadColumnPolicy,
|
|
48493
49019
|
manifestPath,
|
|
48494
49020
|
markdownTable,
|
|
48495
49021
|
memberRoleName,
|
|
@@ -48517,6 +49043,7 @@ function defaultPdfSender(auth) {
|
|
|
48517
49043
|
readToken,
|
|
48518
49044
|
referenceLocalFile,
|
|
48519
49045
|
referenceUrl,
|
|
49046
|
+
regenerateAudienceViewFromDb,
|
|
48520
49047
|
registerNativeEntities,
|
|
48521
49048
|
registryPath,
|
|
48522
49049
|
resolveActiveS3Config,
|
|
@@ -48531,9 +49058,13 @@ function defaultPdfSender(auth) {
|
|
|
48531
49058
|
saveDbCredentialForTeam,
|
|
48532
49059
|
sealUnderSource,
|
|
48533
49060
|
secureCloud,
|
|
49061
|
+
seedColumnPolicyFromYaml,
|
|
48534
49062
|
setActiveWorkspace,
|
|
48535
49063
|
setCloudSetting,
|
|
49064
|
+
setColumnAudience,
|
|
48536
49065
|
setRowVisibility,
|
|
49066
|
+
setTableDefaultVisibility,
|
|
49067
|
+
setTableNeverShare,
|
|
48537
49068
|
shredSource,
|
|
48538
49069
|
slugify,
|
|
48539
49070
|
summarizeText,
|