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/index.js CHANGED
@@ -41438,7 +41438,42 @@ function cleanupEntityContexts(outputDir, entityContexts, currentSlugsByTable, m
41438
41438
  return result;
41439
41439
  }
41440
41440
 
41441
+ // src/render/progress.ts
41442
+ var THROTTLE_WINDOW_MS = 200;
41443
+ var ProgressThrottle = class {
41444
+ cb;
41445
+ windowMs;
41446
+ lastEmit = 0;
41447
+ constructor(cb, windowMs = THROTTLE_WINDOW_MS) {
41448
+ this.cb = cb;
41449
+ this.windowMs = windowMs;
41450
+ }
41451
+ /**
41452
+ * Emit a `table-progress` event, but only if the window since the last
41453
+ * passthrough has elapsed. Dropped events are simply not delivered — the next
41454
+ * one that survives carries the latest running count.
41455
+ */
41456
+ tick(event) {
41457
+ if (!this.cb) return;
41458
+ const now = Date.now();
41459
+ if (now - this.lastEmit < this.windowMs) return;
41460
+ this.lastEmit = now;
41461
+ this.cb(event);
41462
+ }
41463
+ /**
41464
+ * Emit a lifecycle event immediately and reset the throttle window. Use for
41465
+ * `table-start`, `table-done`, `done`, and `error` — none of which should
41466
+ * ever be dropped. Resetting on `table-start` gives each table a clean budget.
41467
+ */
41468
+ force(event) {
41469
+ if (!this.cb) return;
41470
+ this.lastEmit = Date.now();
41471
+ this.cb(event);
41472
+ }
41473
+ };
41474
+
41441
41475
  // src/render/engine.ts
41476
+ var YIELD_EVERY_ENTITIES = 200;
41442
41477
  var NOOP_RENDER = () => "";
41443
41478
  var RenderEngine = class {
41444
41479
  _schema;
@@ -41452,11 +41487,14 @@ var RenderEngine = class {
41452
41487
  this._getTaskContext = getTaskContext ?? (() => "");
41453
41488
  this._skipEmpty = options?.skipEmpty ?? false;
41454
41489
  }
41455
- async render(outputDir) {
41490
+ async render(outputDir, opts = {}) {
41456
41491
  const start = Date.now();
41457
41492
  const filesWritten = [];
41458
41493
  const counters = { skipped: 0 };
41494
+ const signal = opts.signal;
41495
+ const throttle = new ProgressThrottle(opts.onProgress);
41459
41496
  for (const [name, def] of this._schema.getTables()) {
41497
+ if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
41460
41498
  if (this._skipEmpty && def.render === NOOP_RENDER) continue;
41461
41499
  let rows = await this._schema.queryTable(this._adapter, name);
41462
41500
  if (def.relevanceFilter) {
@@ -41498,8 +41536,18 @@ var RenderEngine = class {
41498
41536
  } else {
41499
41537
  counters.skipped++;
41500
41538
  }
41539
+ throttle.force({
41540
+ kind: "table-done",
41541
+ table: name,
41542
+ entitiesRendered: rows.length,
41543
+ entitiesTotal: rows.length,
41544
+ tableIndex: 0,
41545
+ tableCount: 0,
41546
+ pct: 100
41547
+ });
41501
41548
  }
41502
- for (const [, def] of this._schema.getMultis()) {
41549
+ for (const [name, def] of this._schema.getMultis()) {
41550
+ if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
41503
41551
  const keys = await def.keys();
41504
41552
  const tables = {};
41505
41553
  if (def.tables) {
@@ -41516,12 +41564,26 @@ var RenderEngine = class {
41516
41564
  counters.skipped++;
41517
41565
  }
41518
41566
  }
41567
+ throttle.force({
41568
+ kind: "table-done",
41569
+ table: name,
41570
+ entitiesRendered: keys.length,
41571
+ entitiesTotal: keys.length,
41572
+ tableIndex: 0,
41573
+ tableCount: 0,
41574
+ pct: 100
41575
+ });
41519
41576
  }
41520
41577
  const entityContextManifest = await this._renderEntityContexts(
41521
41578
  outputDir,
41522
41579
  filesWritten,
41523
- counters
41580
+ counters,
41581
+ throttle,
41582
+ signal
41524
41583
  );
41584
+ if (entityContextManifest === null) {
41585
+ return this._abortedResult(filesWritten, counters, start);
41586
+ }
41525
41587
  if (this._schema.getEntityContexts().size > 0) {
41526
41588
  writeManifest(outputDir, {
41527
41589
  version: 2,
@@ -41529,6 +41591,29 @@ var RenderEngine = class {
41529
41591
  entityContexts: entityContextManifest
41530
41592
  });
41531
41593
  }
41594
+ const result = {
41595
+ filesWritten,
41596
+ filesSkipped: counters.skipped,
41597
+ durationMs: Date.now() - start
41598
+ };
41599
+ throttle.force({
41600
+ kind: "done",
41601
+ table: null,
41602
+ entitiesRendered: 0,
41603
+ entitiesTotal: 0,
41604
+ tableIndex: 0,
41605
+ tableCount: 0,
41606
+ pct: 100,
41607
+ durationMs: result.durationMs
41608
+ });
41609
+ return result;
41610
+ }
41611
+ /**
41612
+ * Build the partial RenderResult to return when a render is aborted. No
41613
+ * `done` event is emitted — the caller treats abort as "discard the partial
41614
+ * tree", not as a successful completion.
41615
+ */
41616
+ _abortedResult(filesWritten, counters, start) {
41532
41617
  return {
41533
41618
  filesWritten,
41534
41619
  filesSkipped: counters.skipped,
@@ -41564,18 +41649,35 @@ var RenderEngine = class {
41564
41649
  /**
41565
41650
  * Render all entity context definitions.
41566
41651
  * Mutates `filesWritten` and `counters` in place.
41567
- * Returns manifest data for the entity contexts rendered this cycle.
41652
+ * Returns manifest data for the entity contexts rendered this cycle, or
41653
+ * `null` if the render was aborted mid-flight (the caller discards the
41654
+ * partial tree). Progress is reported through `throttle`; abort is observed
41655
+ * via `signal`.
41568
41656
  */
41569
- async _renderEntityContexts(outputDir, filesWritten, counters) {
41657
+ async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal) {
41570
41658
  const manifestData = {};
41571
41659
  const protectedTables = /* @__PURE__ */ new Set();
41572
41660
  for (const [t8, d6] of this._schema.getEntityContexts()) {
41573
41661
  if (d6.protected) protectedTables.add(t8);
41574
41662
  }
41575
- for (const [table, def] of this._schema.getEntityContexts()) {
41663
+ const entityTables = [...this._schema.getEntityContexts()];
41664
+ const tableCount = entityTables.length;
41665
+ for (let tableIndex = 0; tableIndex < tableCount; tableIndex++) {
41666
+ if (signal?.aborted) return null;
41667
+ const [table, def] = entityTables[tableIndex];
41576
41668
  const entityPk = this._schema.getPrimaryKey(table)[0] ?? "id";
41577
41669
  const allRows = await this._schema.queryTable(this._adapter, table);
41578
41670
  const directoryRoot = def.directoryRoot ?? table;
41671
+ const entitiesTotal = allRows.length;
41672
+ throttle.force({
41673
+ kind: "table-start",
41674
+ table,
41675
+ entitiesRendered: 0,
41676
+ entitiesTotal,
41677
+ tableIndex,
41678
+ tableCount,
41679
+ pct: 0
41680
+ });
41579
41681
  const manifestEntry = {
41580
41682
  directoryRoot,
41581
41683
  ...def.index ? { indexFile: def.index.outputFile } : {},
@@ -41591,7 +41693,12 @@ var RenderEngine = class {
41591
41693
  counters.skipped++;
41592
41694
  }
41593
41695
  }
41594
- for (const entityRow of allRows) {
41696
+ for (let i6 = 0; i6 < allRows.length; i6++) {
41697
+ const entityRow = allRows[i6];
41698
+ if (signal?.aborted) return null;
41699
+ if (i6 > 0 && i6 % YIELD_EVERY_ENTITIES === 0) {
41700
+ await new Promise((r6) => setImmediate(r6));
41701
+ }
41595
41702
  const rawSlug = def.slug(entityRow);
41596
41703
  const slug = rawSlug.replace(/[\u00A0\u2000-\u200B\u202F\u205F\u3000]/g, " ").replace(/[\u0000-\u001F\u007F]/g, "");
41597
41704
  if (/[^a-zA-Z0-9.\-_ @(),#&'+:;!~[\]]/.test(slug)) {
@@ -41636,6 +41743,7 @@ var RenderEngine = class {
41636
41743
  const entityFileHashes = {};
41637
41744
  const protection = protectedTables.size > 0 ? { protectedTables, currentTable: table } : void 0;
41638
41745
  for (const [filename, spec] of Object.entries(def.files)) {
41746
+ if (signal?.aborted) return null;
41639
41747
  const mergeDefaults = def.sourceDefaults && spec.source.type !== "self" && spec.source.type !== "custom" && spec.source.type !== "enriched";
41640
41748
  const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
41641
41749
  const rows = await resolveEntitySource(
@@ -41683,8 +41791,27 @@ var RenderEngine = class {
41683
41791
  }
41684
41792
  }
41685
41793
  manifestEntry.entities[slug] = entityFileHashes;
41794
+ const entitiesRendered = i6 + 1;
41795
+ throttle.tick({
41796
+ kind: "table-progress",
41797
+ table,
41798
+ entitiesRendered,
41799
+ entitiesTotal,
41800
+ tableIndex,
41801
+ tableCount,
41802
+ pct: entitiesTotal > 0 ? entitiesRendered / entitiesTotal * 100 : 100
41803
+ });
41686
41804
  }
41687
41805
  manifestData[table] = manifestEntry;
41806
+ throttle.force({
41807
+ kind: "table-done",
41808
+ table,
41809
+ entitiesRendered: entitiesTotal,
41810
+ entitiesTotal,
41811
+ tableIndex,
41812
+ tableCount,
41813
+ pct: 100
41814
+ });
41688
41815
  }
41689
41816
  return manifestData;
41690
41817
  }
@@ -44291,6 +44418,54 @@ var Lattice = class _Lattice {
44291
44418
  async insert(table, row, provenance) {
44292
44419
  const notInit = this._notInitError();
44293
44420
  if (notInit) return notInit;
44421
+ const { sql, values, pkValue, rowWithPk } = this._prepareInsert(table, row);
44422
+ await runAsyncOrSync(this._adapter, sql, values);
44423
+ await this._afterInsert(table, pkValue, rowWithPk, provenance);
44424
+ return pkValue;
44425
+ }
44426
+ /**
44427
+ * Insert a row while atomically forcing its cloud row-visibility, regardless of
44428
+ * the table's `default_row_visibility`. The per-table insert trigger reads a
44429
+ * transaction-local GUC (`lattice.force_row_visibility`); we set it and run the
44430
+ * INSERT inside a single transaction, so the row is stamped at `visibility` the
44431
+ * instant it exists — it is never momentarily visible at the table default, and
44432
+ * the change-feed `NOTIFY` (delivered only at COMMIT) fires when the row already
44433
+ * carries this visibility. This closes the create-then-demote window that a
44434
+ * plain `insert()` + `setRowVisibility()` would leave open.
44435
+ *
44436
+ * Postgres-only: SQLite is single-user (no cross-viewer leak) and has no trigger
44437
+ * to read the GUC, so it degrades to a plain {@link insert}. A `never_share`
44438
+ * table still wins — its rows are forced private even if `visibility` is
44439
+ * `'everyone'` (the trigger enforces that precedence).
44440
+ *
44441
+ * @since 3.1.0
44442
+ */
44443
+ async insertForcingVisibility(table, row, visibility, provenance) {
44444
+ const notInit = this._notInitError();
44445
+ if (notInit) return notInit;
44446
+ const vis = visibility;
44447
+ if (vis !== "private" && vis !== "everyone") {
44448
+ throw new Error(`lattice: invalid forced visibility "${vis}"`);
44449
+ }
44450
+ const withClient = this._adapter.withClient?.bind(this._adapter);
44451
+ if (this.getDialect() !== "postgres" || !withClient) {
44452
+ return this.insert(table, row, provenance);
44453
+ }
44454
+ const { sql, values, pkValue, rowWithPk } = this._prepareInsert(table, row);
44455
+ await withClient(async (tx) => {
44456
+ await tx.run(`SELECT set_config('lattice.force_row_visibility', ?, true)`, [visibility]);
44457
+ await tx.run(sql, values);
44458
+ });
44459
+ await this._afterInsert(table, pkValue, rowWithPk, provenance);
44460
+ return pkValue;
44461
+ }
44462
+ /**
44463
+ * Build the INSERT statement + canonical pk for a row (sanitize → schema-filter →
44464
+ * auto-pk → encrypt). Shared by {@link insert} and {@link insertForcingVisibility}
44465
+ * so both produce byte-identical writes; the latter only differs in running it
44466
+ * inside a GUC-scoped transaction.
44467
+ */
44468
+ _prepareInsert(table, row) {
44294
44469
  this._assertIdent(table);
44295
44470
  const sanitized = this._filterToSchemaColumns(table, this._sanitizer.sanitizeRow(row));
44296
44471
  const pkCols = this._schema.getPrimaryKey(table);
@@ -44306,12 +44481,17 @@ var Lattice = class _Lattice {
44306
44481
  const cols = Object.keys(encrypted).map((c6) => `"${c6}"`).join(", ");
44307
44482
  const placeholders = Object.keys(encrypted).map(() => "?").join(", ");
44308
44483
  const values = Object.values(encrypted);
44309
- await runAsyncOrSync(
44310
- this._adapter,
44311
- `INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`,
44312
- values
44313
- );
44314
44484
  const pkValue = this._serializeRowPk(table, rowWithPk);
44485
+ return {
44486
+ sql: `INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`,
44487
+ values,
44488
+ pkValue,
44489
+ rowWithPk
44490
+ };
44491
+ }
44492
+ /** Post-insert side effects (changelog, audit, write hooks, embedding sync),
44493
+ * identical for the plain and force-visibility insert paths. */
44494
+ async _afterInsert(table, pkValue, rowWithPk, provenance) {
44315
44495
  await this._appendChangelog(
44316
44496
  table,
44317
44497
  pkValue,
@@ -44325,7 +44505,6 @@ var Lattice = class _Lattice {
44325
44505
  this._sanitizer.emitAudit(table, "insert", pkValue);
44326
44506
  await this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
44327
44507
  this._syncEmbedding(table, "insert", rowWithPk, pkValue);
44328
- return pkValue;
44329
44508
  }
44330
44509
  /**
44331
44510
  * Insert a row and return the full inserted row (including auto-generated
@@ -44392,6 +44571,7 @@ var Lattice = class _Lattice {
44392
44571
  const sanitized = this._filterToSchemaColumns(table, this._sanitizer.sanitizeRow(row));
44393
44572
  const encrypted = this._encryptRow(table, sanitized);
44394
44573
  const setCols = Object.keys(encrypted).map((c6) => `"${c6}" = ?`).join(", ");
44574
+ if (setCols === "") return;
44395
44575
  const { clause, params: pkParams } = this._pkWhere(table, id);
44396
44576
  let previousValues = null;
44397
44577
  if (this._changelogTables.has(table)) {
@@ -45008,13 +45188,32 @@ var Lattice = class _Lattice {
45008
45188
  // -------------------------------------------------------------------------
45009
45189
  // Sync
45010
45190
  // -------------------------------------------------------------------------
45011
- async render(outputDir) {
45191
+ async render(outputDir, opts = {}) {
45012
45192
  const notInit = this._notInitError();
45013
45193
  if (notInit) return notInit;
45014
- const result = await this._render.render(outputDir);
45194
+ const result = await this._render.render(outputDir, opts);
45015
45195
  for (const h6 of this._renderHandlers) h6(result);
45016
45196
  return result;
45017
45197
  }
45198
+ /**
45199
+ * Render into `outputDir` through the shared single-flight guard, intended to
45200
+ * be called fire-and-forget (e.g. the GUI's instant-open background render).
45201
+ *
45202
+ * The guard ({@link _renderGuarded}) holds {@link _autoRenderInFlight} for the
45203
+ * render's duration, so a data mutation that lands while this render is in
45204
+ * flight is deferred by {@link _runAutoRender} and coalesced — when this
45205
+ * render settles, `finally` clears the flag and re-arms exactly one follow-up
45206
+ * render via {@link _rearmAutoRenderIfPending}. Net invariant: at most one
45207
+ * render to a given dir at a time.
45208
+ *
45209
+ * Errors propagate to the caller (the GUI surfaces them, never silently swallowed); they are
45210
+ * not swallowed here.
45211
+ */
45212
+ async renderInBackground(outputDir, opts = {}) {
45213
+ const notInit = this._notInitError();
45214
+ if (notInit) return notInit;
45215
+ return this._renderGuarded(outputDir, opts);
45216
+ }
45018
45217
  async sync(outputDir) {
45019
45218
  const notInit = this._notInitError();
45020
45219
  if (notInit) return notInit;
@@ -45356,6 +45555,30 @@ var Lattice = class _Lattice {
45356
45555
  }, this._autoRenderDebounceMs);
45357
45556
  this._autoRenderTimer.unref();
45358
45557
  }
45558
+ /**
45559
+ * Shared single-flight render path used by {@link renderInBackground}.
45560
+ *
45561
+ * Holds {@link _autoRenderInFlight} for the render's duration so the
45562
+ * mutation-driven {@link _runAutoRender} defers while this render runs (it
45563
+ * sees the flag and marks itself pending instead of starting a second,
45564
+ * overlapping render). On settle, `finally` clears the flag and re-arms a
45565
+ * single coalesced follow-up render if any mutation arrived mid-flight.
45566
+ * Errors propagate to the caller; the flag is always cleared.
45567
+ */
45568
+ async _renderGuarded(outputDir, opts) {
45569
+ while (this._autoRenderInFlight) {
45570
+ await new Promise((r6) => setImmediate(r6));
45571
+ }
45572
+ this._autoRenderInFlight = true;
45573
+ try {
45574
+ const result = await this._render.render(outputDir, opts);
45575
+ for (const h6 of this._renderHandlers) h6(result);
45576
+ return result;
45577
+ } finally {
45578
+ this._autoRenderInFlight = false;
45579
+ this._rearmAutoRenderIfPending();
45580
+ }
45581
+ }
45359
45582
  async _runAutoRender() {
45360
45583
  const dir = this._autoRenderDir;
45361
45584
  if (!dir || !this._initialized) return;
@@ -46983,6 +47206,31 @@ function pkSqlExpr(pkCols, prefix) {
46983
47206
  return pkCols.map((c6) => `CAST(${prefix}"${c6}" AS TEXT)`).join(` || chr(9) || `);
46984
47207
  }
46985
47208
  var MEMBER_GROUP = "lattice_members";
47209
+ function pinDefinerSearchPath(sql, schema) {
47210
+ const safe = schema.replace(/"/g, '""');
47211
+ return sql.replace(
47212
+ /SECURITY DEFINER AS/g,
47213
+ `SECURITY DEFINER SET search_path = "${safe}", pg_temp AS`
47214
+ );
47215
+ }
47216
+ async function cloudSchema(db) {
47217
+ const row = await getAsyncOrSync(db.adapter, `SELECT current_schema() AS schema`);
47218
+ const s2 = row?.schema;
47219
+ if (typeof s2 !== "string" || s2.length === 0) {
47220
+ throw new Error("cloud RLS: could not resolve current_schema() for search_path pinning");
47221
+ }
47222
+ return s2;
47223
+ }
47224
+ function revokeSchemaCreateSql(schema) {
47225
+ const lit = `'${schema.replace(/'/g, "''")}'`;
47226
+ return `
47227
+ DO $LATTICE_REVOKE$ BEGIN
47228
+ EXECUTE format('REVOKE CREATE ON SCHEMA %I FROM PUBLIC', ${lit});
47229
+ EXCEPTION WHEN OTHERS THEN
47230
+ NULL; -- not the schema owner, or already revoked
47231
+ END $LATTICE_REVOKE$;
47232
+ `;
47233
+ }
46986
47234
  var CLOUD_RLS_BOOTSTRAP_SQL = `
46987
47235
  -- Member group (NOLOGIN). Members inherit schema/connect/table privileges from it;
46988
47236
  -- RLS filters per the individual member's login role, so the group never widens
@@ -47042,6 +47290,52 @@ CREATE TABLE IF NOT EXISTS "__lattice_cell_grants" (
47042
47290
  PRIMARY KEY ("table_name", "pk", "column_name", "grantee_role")
47043
47291
  );
47044
47292
 
47293
+ -- Per-table policy: the owner-controlled defaults that govern a whole table.
47294
+ -- default_row_visibility is the visibility NEW rows are stamped with (the insert
47295
+ -- trigger reads it); never_share is a hard exclusion \u2014 the share/grant functions
47296
+ -- refuse to elevate such a table and the trigger forces its rows private. Owner-
47297
+ -- managed; members have no grant (it never appears in their data API).
47298
+ CREATE TABLE IF NOT EXISTS "__lattice_table_policy" (
47299
+ "table_name" text PRIMARY KEY,
47300
+ "default_row_visibility" text NOT NULL DEFAULT 'private'
47301
+ CHECK ("default_row_visibility" IN ('private','everyone')),
47302
+ "never_share" boolean NOT NULL DEFAULT false,
47303
+ "updated_by" text NOT NULL DEFAULT session_user,
47304
+ "updated_at" timestamptz NOT NULL DEFAULT now()
47305
+ );
47306
+
47307
+ -- Per-column audience policy: the CANONICAL store of which column carries which
47308
+ -- audience spec (role: / subject: / source: / owner / everyone). Previously the
47309
+ -- spec lived only in the owner's on-disk YAML and was compiled into the mask view
47310
+ -- once at init; storing it here makes it cloud-canonical and member-consistent.
47311
+ -- The generated <table>_v mask view is regenerated from THIS table on change.
47312
+ -- Owner-managed; members have no grant.
47313
+ CREATE TABLE IF NOT EXISTS "__lattice_column_policy" (
47314
+ "table_name" text NOT NULL,
47315
+ "column_name" text NOT NULL,
47316
+ "audience" text NOT NULL,
47317
+ "updated_by" text NOT NULL DEFAULT session_user,
47318
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
47319
+ PRIMARY KEY ("table_name", "column_name")
47320
+ );
47321
+
47322
+ -- Owner-only audit of issued member invites: which scoped role was minted for
47323
+ -- which email (HASHED \u2014 the plaintext email is never stored), when it expires,
47324
+ -- and whether it was redeemed/revoked. No plaintext password is ever stored
47325
+ -- (the credential lives only inside the email-bound token the owner delivers).
47326
+ -- Owner-managed; members have no grant. Named distinctly from any legacy
47327
+ -- team-model invitations table so a pre-existing cloud never collides.
47328
+ CREATE TABLE IF NOT EXISTS "__lattice_member_invites" (
47329
+ "id" text PRIMARY KEY,
47330
+ "role" text NOT NULL,
47331
+ "email_hash" text NOT NULL,
47332
+ "created_by" text NOT NULL DEFAULT session_user,
47333
+ "created_at" timestamptz NOT NULL DEFAULT now(),
47334
+ "expires_at" timestamptz NOT NULL,
47335
+ "redeemed_at" timestamptz,
47336
+ "revoked_at" timestamptz
47337
+ );
47338
+
47045
47339
  -- Visibility check. SECURITY DEFINER so it reads bookkeeping the member can't;
47046
47340
  -- keyed on session_user (the member's login role). A row with no ownership record
47047
47341
  -- is visible to nobody.
@@ -47067,6 +47361,10 @@ BEGIN
47067
47361
  IF p_visibility NOT IN ('private','everyone','custom') THEN
47068
47362
  RAISE EXCEPTION 'lattice: invalid visibility %', p_visibility;
47069
47363
  END IF;
47364
+ IF p_visibility <> 'private'
47365
+ AND COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = p_table), false) THEN
47366
+ RAISE EXCEPTION 'lattice: "%" is a private-only table and cannot be shared', p_table;
47367
+ END IF;
47070
47368
  SELECT o."owner_role" INTO v_owner FROM "__lattice_owners" o
47071
47369
  WHERE o."table_name" = p_table AND o."pk" = p_pk;
47072
47370
  IF v_owner IS NULL THEN RAISE EXCEPTION 'lattice: no ownership record for %/%', p_table, p_pk; END IF;
@@ -47080,6 +47378,9 @@ CREATE OR REPLACE FUNCTION lattice_grant_row(p_table text, p_pk text, p_grantee
47080
47378
  RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
47081
47379
  DECLARE v_owner text;
47082
47380
  BEGIN
47381
+ IF COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = p_table), false) THEN
47382
+ RAISE EXCEPTION 'lattice: "%" is a private-only table and cannot be shared', p_table;
47383
+ END IF;
47083
47384
  SELECT o."owner_role" INTO v_owner FROM "__lattice_owners" o
47084
47385
  WHERE o."table_name" = p_table AND o."pk" = p_pk;
47085
47386
  IF v_owner IS NULL THEN RAISE EXCEPTION 'lattice: no ownership record for %/%', p_table, p_pk; END IF;
@@ -47169,6 +47470,9 @@ CREATE OR REPLACE FUNCTION lattice_grant_cell(p_table text, p_pk text, p_column
47169
47470
  RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
47170
47471
  DECLARE v_owner text;
47171
47472
  BEGIN
47473
+ IF COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = p_table), false) THEN
47474
+ RAISE EXCEPTION 'lattice: "%" is a private-only table and cannot be shared', p_table;
47475
+ END IF;
47172
47476
  SELECT o."owner_role" INTO v_owner FROM "__lattice_owners" o
47173
47477
  WHERE o."table_name" = p_table AND o."pk" = p_pk;
47174
47478
  IF v_owner IS NULL OR v_owner <> session_user THEN
@@ -47201,6 +47505,87 @@ RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
47201
47505
  SELECT lattice_row_visible('files', p_source_ref)
47202
47506
  $fn$;
47203
47507
 
47508
+ -- Is the connected member the OWNER of this row? Used by the "owner" column
47509
+ -- audience (a secret column reveals only to the row owner). SECURITY DEFINER +
47510
+ -- session_user, like the other predicates.
47511
+ CREATE OR REPLACE FUNCTION lattice_is_owner(p_table text, p_pk text)
47512
+ RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
47513
+ SELECT EXISTS (
47514
+ SELECT 1 FROM "__lattice_owners" o
47515
+ WHERE o."table_name" = p_table AND o."pk" = p_pk AND o."owner_role" = session_user
47516
+ )
47517
+ $fn$;
47518
+
47519
+ -- Owner-only: set a table's default row visibility for NEW rows. Raises unless the
47520
+ -- caller can create roles (a cloud owner / DBA), like lattice_assign_role. Rejects
47521
+ -- 'everyone' on a never-share table.
47522
+ CREATE OR REPLACE FUNCTION lattice_set_table_default_visibility(p_table text, p_visibility text)
47523
+ RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
47524
+ BEGIN
47525
+ IF NOT (SELECT rolcreaterole FROM pg_roles WHERE rolname = session_user) THEN
47526
+ RAISE EXCEPTION 'lattice: only a cloud owner may set a table''s default visibility';
47527
+ END IF;
47528
+ IF p_visibility NOT IN ('private','everyone') THEN
47529
+ RAISE EXCEPTION 'lattice: invalid default visibility %', p_visibility;
47530
+ END IF;
47531
+ IF p_visibility = 'everyone'
47532
+ AND COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = p_table), false) THEN
47533
+ RAISE EXCEPTION 'lattice: "%" is a private-only table; its rows cannot default to everyone', p_table;
47534
+ END IF;
47535
+ INSERT INTO "__lattice_table_policy" ("table_name","default_row_visibility","updated_by","updated_at")
47536
+ VALUES (p_table, p_visibility, session_user, now())
47537
+ ON CONFLICT ("table_name") DO UPDATE
47538
+ SET "default_row_visibility" = EXCLUDED."default_row_visibility",
47539
+ "updated_by" = session_user, "updated_at" = now();
47540
+ END $fn$;
47541
+
47542
+ -- Owner-only: mark a table never-shareable (Secrets/Messages-class). When true the
47543
+ -- share/grant functions raise and the insert trigger forces new rows private; the
47544
+ -- default visibility is also forced private. Turning it ON also RETROACTIVELY
47545
+ -- privatizes the table: any row currently shared ('everyone'/'custom') is reset to
47546
+ -- 'private' and every existing row/cell grant on the table is dropped \u2014 otherwise
47547
+ -- flagging a table never-share would leave already-leaked rows visible, defeating
47548
+ -- the point. Idempotent: re-running with already-private rows updates nothing.
47549
+ CREATE OR REPLACE FUNCTION lattice_set_table_never_share(p_table text, p_on boolean)
47550
+ RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
47551
+ BEGIN
47552
+ IF NOT (SELECT rolcreaterole FROM pg_roles WHERE rolname = session_user) THEN
47553
+ RAISE EXCEPTION 'lattice: only a cloud owner may change a table''s never-share flag';
47554
+ END IF;
47555
+ INSERT INTO "__lattice_table_policy" ("table_name","never_share","default_row_visibility","updated_by","updated_at")
47556
+ VALUES (p_table, p_on, CASE WHEN p_on THEN 'private' ELSE 'private' END, session_user, now())
47557
+ ON CONFLICT ("table_name") DO UPDATE
47558
+ SET "never_share" = EXCLUDED."never_share",
47559
+ "default_row_visibility" = CASE WHEN EXCLUDED."never_share"
47560
+ THEN 'private' ELSE "__lattice_table_policy"."default_row_visibility" END,
47561
+ "updated_by" = session_user, "updated_at" = now();
47562
+ IF p_on THEN
47563
+ UPDATE "__lattice_owners" SET "visibility" = 'private', "updated_at" = now()
47564
+ WHERE "table_name" = p_table AND "visibility" <> 'private';
47565
+ DELETE FROM "__lattice_row_grants" WHERE "table_name" = p_table;
47566
+ DELETE FROM "__lattice_cell_grants" WHERE "table_name" = p_table;
47567
+ END IF;
47568
+ END $fn$;
47569
+
47570
+ -- Owner-only: set (or clear) a column's audience spec in the canonical DB store.
47571
+ -- An empty/null spec removes the policy row (column becomes unmasked). The GUI/lib
47572
+ -- regenerates the table's mask view from this store after calling this.
47573
+ CREATE OR REPLACE FUNCTION lattice_set_column_audience(p_table text, p_column text, p_audience text)
47574
+ RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
47575
+ BEGIN
47576
+ IF NOT (SELECT rolcreaterole FROM pg_roles WHERE rolname = session_user) THEN
47577
+ RAISE EXCEPTION 'lattice: only a cloud owner may set a column audience';
47578
+ END IF;
47579
+ IF p_audience IS NULL OR btrim(p_audience) = '' THEN
47580
+ DELETE FROM "__lattice_column_policy" WHERE "table_name" = p_table AND "column_name" = p_column;
47581
+ ELSE
47582
+ INSERT INTO "__lattice_column_policy" ("table_name","column_name","audience","updated_by","updated_at")
47583
+ VALUES (p_table, p_column, p_audience, session_user, now())
47584
+ ON CONFLICT ("table_name","column_name") DO UPDATE
47585
+ SET "audience" = EXCLUDED."audience", "updated_by" = session_user, "updated_at" = now();
47586
+ END IF;
47587
+ END $fn$;
47588
+
47204
47589
  -- Append-only change feed. The per-table ownership trigger records one row per
47205
47590
  -- INSERT/UPDATE/DELETE; the AFTER INSERT trigger here fires pg_notify so a
47206
47591
  -- connected member's realtime broker refreshes. Members get no direct access \u2014
@@ -47245,7 +47630,20 @@ CREATE OR REPLACE FUNCTION "${trg}"() RETURNS trigger LANGUAGE plpgsql SECURITY
47245
47630
  BEGIN
47246
47631
  IF TG_OP = 'INSERT' THEN
47247
47632
  INSERT INTO "__lattice_owners" ("table_name","pk","owner_role","visibility")
47248
- VALUES (${lit}, ${pkNew}, session_user, 'private')
47633
+ VALUES (${lit}, ${pkNew}, session_user,
47634
+ CASE
47635
+ -- never-share always wins: such a table's rows are private, full stop.
47636
+ WHEN COALESCE((SELECT "never_share" FROM "__lattice_table_policy" WHERE "table_name" = ${lit}), false)
47637
+ THEN 'private'
47638
+ -- per-INSERT override: a caller forcing visibility for THIS write (e.g.
47639
+ -- chat "private mode") sets the transaction-local lattice.force_row_visibility
47640
+ -- GUC, so the row is stamped atomically at insert \u2014 never momentarily at
47641
+ -- the table default, and the change-feed NOTIFY (deferred to COMMIT) only
47642
+ -- fires once the row already carries this visibility.
47643
+ WHEN NULLIF(current_setting('lattice.force_row_visibility', true), '') IN ('private','everyone')
47644
+ THEN current_setting('lattice.force_row_visibility', true)
47645
+ ELSE COALESCE((SELECT "default_row_visibility" FROM "__lattice_table_policy" WHERE "table_name" = ${lit}), 'private')
47646
+ END)
47249
47647
  ON CONFLICT ("table_name","pk") DO NOTHING;
47250
47648
  INSERT INTO "__lattice_changes" ("table_name","pk","op","owner_role")
47251
47649
  VALUES (${lit}, ${pkNew}, 'upsert', session_user);
@@ -47285,19 +47683,28 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
47285
47683
  }
47286
47684
  async function installCloudRls(db) {
47287
47685
  if (!isPg(db)) return;
47686
+ const schema = await cloudSchema(db);
47288
47687
  const migration = {
47289
47688
  // v3 added the audience helpers; v4 the role model; v5 the per-card override
47290
- // model (__lattice_cell_grants + lattice_cell_visible / lattice_grant_cell).
47291
- // The bootstrap is fully idempotent (CREATE OR REPLACE / IF NOT EXISTS).
47292
- version: "internal:cloud-rls:bootstrap:v5",
47293
- sql: CLOUD_RLS_BOOTSTRAP_SQL
47689
+ // model (__lattice_cell_grants + lattice_cell_visible / lattice_grant_cell);
47690
+ // v6 added per-table policy (__lattice_table_policy: default_row_visibility +
47691
+ // never_share, enforced in the insert trigger + share/grant guards), the
47692
+ // canonical column-audience store (__lattice_column_policy), lattice_is_owner,
47693
+ // and the owner-only setters; v7 pins search_path on every SECURITY DEFINER
47694
+ // helper (closes the pg_temp-shadow RLS bypass) + revokes schema CREATE from
47695
+ // PUBLIC. The bootstrap is fully idempotent.
47696
+ version: "internal:cloud-rls:bootstrap:v7",
47697
+ sql: pinDefinerSearchPath(CLOUD_RLS_BOOTSTRAP_SQL, schema) + revokeSchemaCreateSql(schema)
47294
47698
  };
47295
47699
  await db.migrate([migration]);
47296
47700
  }
47297
47701
  async function enableChangelogRls(db) {
47298
47702
  if (!isPg(db)) return;
47299
47703
  const migration = {
47300
- version: "internal:cloud-rls:changelog:v1",
47704
+ // v2: ground-truth/audit entries are owner-only (was lattice_row_visible),
47705
+ // closing the masked-column-via-history leak. Bump re-installs the policy on
47706
+ // existing clouds.
47707
+ version: "internal:cloud-rls:changelog:v2",
47301
47708
  sql: `
47302
47709
  ALTER TABLE "__lattice_changelog" ENABLE ROW LEVEL SECURITY;
47303
47710
  ALTER TABLE "__lattice_changelog" FORCE ROW LEVEL SECURITY;
@@ -47312,7 +47719,7 @@ CREATE POLICY "lattice_changelog_sel" ON "__lattice_changelog" FOR SELECT USING
47312
47719
  SELECT 1 FROM jsonb_array_elements_text("source_ref"::jsonb) AS src(sid)
47313
47720
  WHERE NOT lattice_source_visible(src.sid)
47314
47721
  )
47315
- ELSE lattice_row_visible("table_name", "row_id")
47722
+ ELSE lattice_is_owner("table_name", "row_id")
47316
47723
  END
47317
47724
  );
47318
47725
  DROP POLICY IF EXISTS "lattice_changelog_ins" ON "__lattice_changelog";
@@ -47323,9 +47730,10 @@ CREATE POLICY "lattice_changelog_ins" ON "__lattice_changelog" FOR INSERT WITH C
47323
47730
  }
47324
47731
  async function enableRlsForTable(db, table, pkCols) {
47325
47732
  if (!isPg(db)) return;
47733
+ const schema = await cloudSchema(db);
47326
47734
  const migration = {
47327
- version: `internal:cloud-rls:table:${table}:v2`,
47328
- sql: tableRlsSql(table, pkCols)
47735
+ version: `internal:cloud-rls:table:${table}:v3`,
47736
+ sql: pinDefinerSearchPath(tableRlsSql(table, pkCols), schema)
47329
47737
  };
47330
47738
  await db.migrate([migration]);
47331
47739
  }
@@ -47455,12 +47863,17 @@ function isRowAudience(audience) {
47455
47863
  const a6 = (audience ?? "").trim();
47456
47864
  return a6 === "" || a6 === "everyone" || a6 === "row-audience";
47457
47865
  }
47458
- function audiencePredicate(audience) {
47866
+ function audiencePredicate(audience, ctx) {
47459
47867
  if (isRowAudience(audience)) return "true";
47460
47868
  const clauses = audience.split("+").map((c6) => c6.trim()).filter(Boolean);
47461
47869
  const parts = [];
47462
47870
  for (const clause of clauses) {
47463
47871
  if (clause === "everyone" || clause === "row-audience") return "true";
47872
+ if (clause === "owner") {
47873
+ if (!ctx) throw new Error('lattice: the "owner" audience needs a row context');
47874
+ parts.push(`lattice_is_owner(${ctx.tableLit}, ${ctx.pkExpr})`);
47875
+ continue;
47876
+ }
47464
47877
  const idx = clause.indexOf(":");
47465
47878
  const kind = idx === -1 ? clause : clause.slice(0, idx);
47466
47879
  const arg = idx === -1 ? "" : clause.slice(idx + 1).trim();
@@ -47495,7 +47908,7 @@ function audienceViewSql(table, columns, pkCols, columnAudience) {
47495
47908
  const selectCols = columns.map((col) => {
47496
47909
  const aud = columnAudience[col] ?? "";
47497
47910
  if (isRowAudience(aud)) return quoteIdent(col);
47498
- const pred = audiencePredicate(aud);
47911
+ const pred = audiencePredicate(aud, { tableLit: lit, pkExpr });
47499
47912
  if (pred === "true") return quoteIdent(col);
47500
47913
  const colLit = `'${col.replace(/'/g, "''")}'`;
47501
47914
  const full = `(${pred}) OR lattice_cell_visible(${lit}, ${pkExpr}, ${colLit})`;
@@ -47530,6 +47943,92 @@ async function enableAudienceView(db, table, columns, pkCols, columnAudience) {
47530
47943
  };
47531
47944
  await db.migrate([migration]);
47532
47945
  }
47946
+ async function loadColumnPolicy(db, table) {
47947
+ if (db.getDialect() !== "postgres") return {};
47948
+ const rows = await allAsyncOrSync(
47949
+ db.adapter,
47950
+ `SELECT "column_name", "audience" FROM "__lattice_column_policy" WHERE "table_name" = ?`,
47951
+ [table]
47952
+ );
47953
+ const out = {};
47954
+ for (const r6 of rows) out[r6.column_name] = r6.audience;
47955
+ return out;
47956
+ }
47957
+ async function seedColumnPolicyFromYaml(db, table, yamlAudience) {
47958
+ if (db.getDialect() !== "postgres") return;
47959
+ const marker = `internal:cloud-column-seed:${table}:v1`;
47960
+ const already = await getAsyncOrSync(
47961
+ db.adapter,
47962
+ `SELECT 1 AS one FROM "__lattice_migrations" WHERE "version" = ?`,
47963
+ [marker]
47964
+ );
47965
+ if (already) return;
47966
+ for (const [col, aud] of Object.entries(yamlAudience)) {
47967
+ if (isRowAudience(aud)) continue;
47968
+ await runAsyncOrSync(
47969
+ db.adapter,
47970
+ `INSERT INTO "__lattice_column_policy" ("table_name","column_name","audience")
47971
+ VALUES (?, ?, ?) ON CONFLICT ("table_name","column_name") DO NOTHING`,
47972
+ [table, col, aud]
47973
+ );
47974
+ }
47975
+ await runAsyncOrSync(
47976
+ db.adapter,
47977
+ `INSERT INTO "__lattice_migrations" ("version","applied_at") VALUES (?, ?)
47978
+ ON CONFLICT ("version") DO NOTHING`,
47979
+ [marker, (/* @__PURE__ */ new Date()).toISOString()]
47980
+ );
47981
+ }
47982
+ async function regenerateAudienceViewFromDb(db, table, columns, pkCols) {
47983
+ if (db.getDialect() !== "postgres") return;
47984
+ if (pkCols.length === 0) return;
47985
+ const spec = await loadColumnPolicy(db, table);
47986
+ const view = quoteIdent(`${table}_v`);
47987
+ const base = quoteIdent(table);
47988
+ if (!tableNeedsAudienceView(spec)) {
47989
+ await runAsyncOrSync(
47990
+ db.adapter,
47991
+ `DROP VIEW IF EXISTS ${view};
47992
+ GRANT SELECT ON ${base} TO ${MEMBER_GROUP};`
47993
+ );
47994
+ return;
47995
+ }
47996
+ await runAsyncOrSync(db.adapter, audienceViewSql(table, columns, pkCols, spec));
47997
+ }
47998
+ async function setColumnAudience(db, table, column, audience, columns, pkCols) {
47999
+ if (db.getDialect() !== "postgres") return;
48000
+ await runAsyncOrSync(db.adapter, `SELECT lattice_set_column_audience(?, ?, ?)`, [
48001
+ table,
48002
+ column,
48003
+ audience
48004
+ ]);
48005
+ await regenerateAudienceViewFromDb(db, table, columns, pkCols);
48006
+ }
48007
+
48008
+ // src/cloud/table-policy.ts
48009
+ async function getTablePolicy(db, table) {
48010
+ if (db.getDialect() !== "postgres") return { defaultRowVisibility: "private", neverShare: false };
48011
+ const row = await getAsyncOrSync(
48012
+ db.adapter,
48013
+ `SELECT "default_row_visibility", "never_share" FROM "__lattice_table_policy" WHERE "table_name" = ?`,
48014
+ [table]
48015
+ );
48016
+ return {
48017
+ defaultRowVisibility: row?.default_row_visibility === "everyone" ? "everyone" : "private",
48018
+ neverShare: row?.never_share === true
48019
+ };
48020
+ }
48021
+ async function setTableDefaultVisibility(db, table, visibility) {
48022
+ if (db.getDialect() !== "postgres") return;
48023
+ await runAsyncOrSync(db.adapter, `SELECT lattice_set_table_default_visibility(?, ?)`, [
48024
+ table,
48025
+ visibility
48026
+ ]);
48027
+ }
48028
+ async function setTableNeverShare(db, table, on) {
48029
+ if (db.getDialect() !== "postgres") return;
48030
+ await runAsyncOrSync(db.adapter, `SELECT lattice_set_table_never_share(?, ?)`, [table, on]);
48031
+ }
47533
48032
 
47534
48033
  // src/cloud/fold-cache.ts
47535
48034
  function viewerSignature(viewer) {
@@ -47604,9 +48103,12 @@ END $fn$;
47604
48103
  `;
47605
48104
  async function installCloudSettings(db) {
47606
48105
  if (db.getDialect() !== "postgres") return;
48106
+ const schema = await cloudSchema(db);
47607
48107
  const migration = {
47608
- version: "internal:cloud-settings:v1",
47609
- sql: CLOUD_SETTINGS_BOOTSTRAP_SQL
48108
+ // v2 pins search_path on the two SECURITY DEFINER helpers (closes the
48109
+ // pg_temp-shadow class of bypass on the settings getter/setter).
48110
+ version: "internal:cloud-settings:v2",
48111
+ sql: pinDefinerSearchPath(CLOUD_SETTINGS_BOOTSTRAP_SQL, schema)
47610
48112
  };
47611
48113
  await db.migrate([migration]);
47612
48114
  }
@@ -47633,7 +48135,8 @@ async function secureCloud(db) {
47633
48135
  await installCloudSettings(db);
47634
48136
  await db.ensureObservationSubstrate();
47635
48137
  await enableChangelogRls(db);
47636
- for (const table of db.getRegisteredTableNames()) {
48138
+ const registered = db.getRegisteredTableNames();
48139
+ for (const table of registered) {
47637
48140
  if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) continue;
47638
48141
  const pk = db.getPrimaryKey(table);
47639
48142
  if (pk.length === 0) continue;
@@ -47641,9 +48144,21 @@ async function secureCloud(db) {
47641
48144
  await enableRlsForTable(db, table, pk);
47642
48145
  const cols = db.getRegisteredColumns(table);
47643
48146
  if (cols) {
47644
- await enableAudienceView(db, table, Object.keys(cols), pk, db.getColumnAudience(table));
48147
+ await seedColumnPolicyFromYaml(db, table, db.getColumnAudience(table));
48148
+ await regenerateAudienceViewFromDb(db, table, Object.keys(cols), pk);
47645
48149
  }
47646
48150
  }
48151
+ if (registered.includes("secrets")) {
48152
+ await runAsyncOrSync(db.adapter, `SELECT lattice_set_table_never_share('secrets', true)`);
48153
+ }
48154
+ await runAsyncOrSync(
48155
+ db.adapter,
48156
+ `DO $LATTICE$ BEGIN
48157
+ IF to_regclass('__lattice_user_identity') IS NOT NULL THEN
48158
+ EXECUTE 'GRANT SELECT, INSERT, UPDATE ON "__lattice_user_identity" TO ${MEMBER_GROUP}';
48159
+ END IF;
48160
+ END $LATTICE$`
48161
+ );
47647
48162
  }
47648
48163
 
47649
48164
  // src/ai/llm-client.ts
@@ -48237,6 +48752,7 @@ export {
48237
48752
  NATIVE_ENTITY_NAMES,
48238
48753
  NATIVE_REGISTRY_TABLE,
48239
48754
  PostgresAdapter,
48755
+ ProgressThrottle,
48240
48756
  READ_ONLY_HEADER,
48241
48757
  ROOT_DIRNAME,
48242
48758
  ReferenceUnavailableError,
@@ -48300,6 +48816,7 @@ export {
48300
48816
  getCloudSetting,
48301
48817
  getDbCredential,
48302
48818
  getOrCreateMasterKey,
48819
+ getTablePolicy,
48303
48820
  getWorkspace,
48304
48821
  grantCell,
48305
48822
  hasFtsIndex,
@@ -48317,6 +48834,7 @@ export {
48317
48834
  listNativeBindings,
48318
48835
  listTokens,
48319
48836
  listWorkspaces,
48837
+ loadColumnPolicy,
48320
48838
  manifestPath,
48321
48839
  markdownTable,
48322
48840
  memberRoleName,
@@ -48344,6 +48862,7 @@ export {
48344
48862
  readToken,
48345
48863
  referenceLocalFile,
48346
48864
  referenceUrl,
48865
+ regenerateAudienceViewFromDb,
48347
48866
  registerNativeEntities,
48348
48867
  registryPath,
48349
48868
  resolveActiveS3Config,
@@ -48358,9 +48877,13 @@ export {
48358
48877
  saveDbCredentialForTeam,
48359
48878
  sealUnderSource,
48360
48879
  secureCloud,
48880
+ seedColumnPolicyFromYaml,
48361
48881
  setActiveWorkspace,
48362
48882
  setCloudSetting,
48883
+ setColumnAudience,
48363
48884
  setRowVisibility,
48885
+ setTableDefaultVisibility,
48886
+ setTableNeverShare,
48364
48887
  shredSource,
48365
48888
  slugify,
48366
48889
  summarizeText,