latticesql 3.4.5 → 3.4.7

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 CHANGED
@@ -10313,6 +10313,21 @@ var init_registry = __esm({
10313
10313
  ["table", "values"]
10314
10314
  )
10315
10315
  },
10316
+ {
10317
+ name: "create_secret",
10318
+ description: "Store a secret/credential \u2014 an API key, password, OAuth token, connection string, etc. \u2014 by name in the encrypted secrets store. Use this whenever the user gives you a credential to save or asks you to remember/store a secret. WRITE-ONLY: you can save a secret but you can NEVER read, list, echo, or retrieve existing secret values \u2014 they are hidden from you. The value is encrypted at rest.",
10319
+ mutates: true,
10320
+ category: "row",
10321
+ args: obj(
10322
+ {
10323
+ name: str('A short label for the secret, e.g. "GitHub password" or "OpenAI API key".'),
10324
+ value: str("The secret value to store."),
10325
+ kind: str('Optional kind, e.g. "password", "api_key", "token", "connection_string".'),
10326
+ description: str("Optional note about what the secret is for.")
10327
+ },
10328
+ ["name", "value"]
10329
+ )
10330
+ },
10316
10331
  {
10317
10332
  name: "create_artifact",
10318
10333
  description: "Create a markdown document and save it as a file artifact. Use this whenever the user asks you to create, write, draft, or make a document, note, write-up, summary, report, or file \u2014 you author the content as GitHub-flavored markdown and it is saved in the files entity as a markdown artifact, then opened in the viewer for them. Prefer this over create_row on files for any document the user wants to keep. It follows the same sharing rules as any file (private mode \u2192 private).",
@@ -13156,6 +13171,7 @@ var init_ingest_url = __esm({
13156
13171
  });
13157
13172
 
13158
13173
  // src/gui/ai/dispatch.ts
13174
+ import { randomUUID } from "crypto";
13159
13175
  function visibilityDenialReason(opts) {
13160
13176
  if (opts.kind === "table") {
13161
13177
  return opts.canManageTableDefault ? null : "Only the workspace owner can change a table's default sharing.";
@@ -13371,6 +13387,21 @@ async function executeFunction(ctx, name, args) {
13371
13387
  );
13372
13388
  return { ok: true, result: { id } };
13373
13389
  }
13390
+ case "create_secret": {
13391
+ const secretName = requireString(args.name, "name");
13392
+ const secretValue2 = requireString(args.value, "value");
13393
+ const kind = typeof args.kind === "string" && args.kind ? args.kind : null;
13394
+ const description = typeof args.description === "string" && args.description ? args.description : null;
13395
+ const id = randomUUID();
13396
+ await ctx.db.insert("secrets", {
13397
+ id,
13398
+ name: secretName,
13399
+ value: secretValue2,
13400
+ kind,
13401
+ description
13402
+ });
13403
+ return { ok: true, result: { id, name: secretName } };
13404
+ }
13374
13405
  case "create_artifact": {
13375
13406
  const table = requireTable("files", ctx.validTables);
13376
13407
  const title = requireString(args.title, "title");
@@ -13701,6 +13732,7 @@ var init_dispatch = __esm({
13701
13732
  "get_history",
13702
13733
  "create_row",
13703
13734
  "create_artifact",
13735
+ "create_secret",
13704
13736
  "ingest_url",
13705
13737
  "set_definition",
13706
13738
  "set_visibility",
@@ -56049,9 +56081,41 @@ var appJs = `
56049
56081
  }
56050
56082
  function reloadForUpdate(label) {
56051
56083
  if (reloadingForUpdate) return;
56084
+ // Guard against an infinite reload loop. Reloading when the server version
56085
+ // changes is the seamless-update trigger \u2014 but if the version the page was
56086
+ // SERVED with keeps disagreeing with /api/version (e.g. a stale or duplicate
56087
+ // server is holding the port and reporting a different version), every fresh
56088
+ // page would reload again. That unbounded loop pegs memory and crashes the
56089
+ // browser. Cap reloads to MAX within WINDOW; past that, stop and surface the
56090
+ // mismatch instead of spinning. A genuine update reloads once and then the
56091
+ // versions agree, which clears the counter (see checkServerVersion).
56092
+ var KEY = 'lattice:updateReloads',
56093
+ MAX = 3,
56094
+ WINDOW = 60000,
56095
+ now = Date.now(),
56096
+ recent = [];
56097
+ try {
56098
+ recent = (JSON.parse(sessionStorage.getItem(KEY) || '[]') || []).filter(function (t) {
56099
+ return now - t < WINDOW;
56100
+ });
56101
+ } catch (_) {
56102
+ /* sessionStorage blocked \u2014 degrade to a single best-effort reload below */
56103
+ }
56104
+ if (recent.length >= MAX) {
56105
+ showUpdatePill('Version mismatch \u2014 stopped auto-reloading. Reload manually if needed.');
56106
+ return;
56107
+ }
56108
+ recent.push(now);
56109
+ try {
56110
+ sessionStorage.setItem(KEY, JSON.stringify(recent));
56111
+ } catch (_) {
56112
+ /* best-effort */
56113
+ }
56052
56114
  reloadingForUpdate = true;
56053
56115
  showUpdatePill(label || 'Updated \u2014 reloading\u2026');
56054
- setTimeout(function () { location.reload(); }, 600);
56116
+ setTimeout(function () {
56117
+ location.reload();
56118
+ }, 600);
56055
56119
  }
56056
56120
  // Manual upgrade fallback: show an "Update available \u2014 Upgrade" link next to
56057
56121
  // the version chip only when the server reports a newer, installable version.
@@ -56083,7 +56147,16 @@ var appJs = `
56083
56147
  var v = d && d.version ? String(d.version).replace(/^v/, '').trim() : '';
56084
56148
  if (!v) return;
56085
56149
  if (v !== BOOT_VERSION) reloadForUpdate('Updated to v' + v + ' \u2014 reloading\u2026');
56086
- else hideUpdatePill();
56150
+ else {
56151
+ hideUpdatePill();
56152
+ // Versions agree \u2014 clear the reload-loop guard so a later genuine
56153
+ // update can reload again from a clean slate.
56154
+ try {
56155
+ sessionStorage.removeItem('lattice:updateReloads');
56156
+ } catch (_) {
56157
+ /* best-effort */
56158
+ }
56159
+ }
56087
56160
  })
56088
56161
  .catch(function () { /* offline / mid-restart \u2014 the next reconnect retries */ });
56089
56162
  // Refresh the manual-upgrade link alongside the reconnect version check.
@@ -64852,7 +64925,7 @@ function redeemInviteToken(email, token) {
64852
64925
  init_markdown();
64853
64926
  init_rls();
64854
64927
  init_adapter();
64855
- import { randomUUID } from "crypto";
64928
+ import { randomUUID as randomUUID2 } from "crypto";
64856
64929
 
64857
64930
  // src/framework/cloud-migration.ts
64858
64931
  init_lattice();
@@ -65647,7 +65720,7 @@ async function dispatchDbConfigRoute(req, res, ctx) {
65647
65720
  `INSERT INTO "__lattice_member_invites" ("id","role","email_hash","email","expires_at")
65648
65721
  VALUES (?, ?, ?, ?, ?)`,
65649
65722
  [
65650
- randomUUID(),
65723
+ randomUUID2(),
65651
65724
  role,
65652
65725
  emailHash,
65653
65726
  // Plaintext email stored ONLY in this owner-only table so the owner's
@@ -69987,6 +70060,28 @@ ${e6.stack ?? ""}`
69987
70060
  };
69988
70061
  }
69989
70062
 
70063
+ // src/gui/probe-running.ts
70064
+ async function probeRunningGui(port, timeoutMs = 1e3) {
70065
+ const controller = new AbortController();
70066
+ const timer = setTimeout(() => {
70067
+ controller.abort();
70068
+ }, timeoutMs);
70069
+ try {
70070
+ const res = await fetch(`http://127.0.0.1:${String(port)}/api/version`, {
70071
+ signal: controller.signal
70072
+ });
70073
+ if (!res.ok) return null;
70074
+ const data = await res.json();
70075
+ if (typeof data !== "object" || data === null || !("version" in data)) return null;
70076
+ const v2 = data.version;
70077
+ return { version: typeof v2 === "string" ? v2 : "" };
70078
+ } catch {
70079
+ return null;
70080
+ } finally {
70081
+ clearTimeout(timer);
70082
+ }
70083
+ }
70084
+
69990
70085
  // src/gui/supervisor.ts
69991
70086
  import { spawn as spawn3 } from "child_process";
69992
70087
  async function superviseGui(opts) {
@@ -70221,7 +70316,7 @@ function printHelp() {
70221
70316
  );
70222
70317
  }
70223
70318
  function getVersion() {
70224
- if (true) return "3.4.5";
70319
+ if (true) return "3.4.7";
70225
70320
  try {
70226
70321
  const pkgPath = new URL("../package.json", import.meta.url).pathname;
70227
70322
  const pkg = JSON.parse(readFileSync20(pkgPath, "utf-8"));
@@ -70418,6 +70513,18 @@ async function runWatch(args) {
70418
70513
  process.on("SIGTERM", shutdown);
70419
70514
  }
70420
70515
  async function runGui(args) {
70516
+ const port = args.port;
70517
+ if (!process.env.LATTICE_GUI_SUPERVISED) {
70518
+ const running = await probeRunningGui(port);
70519
+ if (running) {
70520
+ const url = `http://127.0.0.1:${String(port)}/`;
70521
+ console.log(
70522
+ `Lattice is already running at ${url}${running.version ? ` (v${running.version})` : ""} \u2014 opening it.`
70523
+ );
70524
+ if (!args.noOpen) openUrl(url);
70525
+ return;
70526
+ }
70527
+ }
70421
70528
  if (!process.env.LATTICE_GUI_SUPERVISED && detectInstallContext().installable) {
70422
70529
  try {
70423
70530
  await superviseGui({
@@ -70445,7 +70552,7 @@ async function runGui(args) {
70445
70552
  configPath: boot.configPath,
70446
70553
  outputDir: boot.contextDir,
70447
70554
  latticeRoot: boot.root,
70448
- port: args.port,
70555
+ port,
70449
70556
  openBrowser: !args.noOpen,
70450
70557
  autoRender: true,
70451
70558
  version: getVersion(),
package/dist/index.cjs CHANGED
@@ -50606,6 +50606,21 @@ var init_registry = __esm({
50606
50606
  ["table", "values"]
50607
50607
  )
50608
50608
  },
50609
+ {
50610
+ name: "create_secret",
50611
+ description: "Store a secret/credential \u2014 an API key, password, OAuth token, connection string, etc. \u2014 by name in the encrypted secrets store. Use this whenever the user gives you a credential to save or asks you to remember/store a secret. WRITE-ONLY: you can save a secret but you can NEVER read, list, echo, or retrieve existing secret values \u2014 they are hidden from you. The value is encrypted at rest.",
50612
+ mutates: true,
50613
+ category: "row",
50614
+ args: obj(
50615
+ {
50616
+ name: str('A short label for the secret, e.g. "GitHub password" or "OpenAI API key".'),
50617
+ value: str("The secret value to store."),
50618
+ kind: str('Optional kind, e.g. "password", "api_key", "token", "connection_string".'),
50619
+ description: str("Optional note about what the secret is for.")
50620
+ },
50621
+ ["name", "value"]
50622
+ )
50623
+ },
50609
50624
  {
50610
50625
  name: "create_artifact",
50611
50626
  description: "Create a markdown document and save it as a file artifact. Use this whenever the user asks you to create, write, draft, or make a document, note, write-up, summary, report, or file \u2014 you author the content as GitHub-flavored markdown and it is saved in the files entity as a markdown artifact, then opened in the viewer for them. Prefer this over create_row on files for any document the user wants to keep. It follows the same sharing rules as any file (private mode \u2192 private).",
@@ -53028,6 +53043,21 @@ async function executeFunction(ctx, name, args) {
53028
53043
  );
53029
53044
  return { ok: true, result: { id } };
53030
53045
  }
53046
+ case "create_secret": {
53047
+ const secretName = requireString(args.name, "name");
53048
+ const secretValue2 = requireString(args.value, "value");
53049
+ const kind = typeof args.kind === "string" && args.kind ? args.kind : null;
53050
+ const description = typeof args.description === "string" && args.description ? args.description : null;
53051
+ const id = (0, import_node_crypto18.randomUUID)();
53052
+ await ctx.db.insert("secrets", {
53053
+ id,
53054
+ name: secretName,
53055
+ value: secretValue2,
53056
+ kind,
53057
+ description
53058
+ });
53059
+ return { ok: true, result: { id, name: secretName } };
53060
+ }
53031
53061
  case "create_artifact": {
53032
53062
  const table = requireTable("files", ctx.validTables);
53033
53063
  const title = requireString(args.title, "title");
@@ -53331,10 +53361,11 @@ async function executeFunction(ctx, name, args) {
53331
53361
  return { ok: false, error: e6.message };
53332
53362
  }
53333
53363
  }
53334
- var DISPATCHABLE, ASSISTANT_HIDDEN_TABLES, SECRET_MASK, BULK_FILTER_OPS;
53364
+ var import_node_crypto18, DISPATCHABLE, ASSISTANT_HIDDEN_TABLES, SECRET_MASK, BULK_FILTER_OPS;
53335
53365
  var init_dispatch = __esm({
53336
53366
  "src/gui/ai/dispatch.ts"() {
53337
53367
  "use strict";
53368
+ import_node_crypto18 = require("crypto");
53338
53369
  init_registry();
53339
53370
  init_lattice_docs();
53340
53371
  init_fts();
@@ -53358,6 +53389,7 @@ var init_dispatch = __esm({
53358
53389
  "get_history",
53359
53390
  "create_row",
53360
53391
  "create_artifact",
53392
+ "create_secret",
53361
53393
  "ingest_url",
53362
53394
  "set_definition",
53363
53395
  "set_visibility",
@@ -57927,9 +57959,41 @@ var appJs = `
57927
57959
  }
57928
57960
  function reloadForUpdate(label) {
57929
57961
  if (reloadingForUpdate) return;
57962
+ // Guard against an infinite reload loop. Reloading when the server version
57963
+ // changes is the seamless-update trigger \u2014 but if the version the page was
57964
+ // SERVED with keeps disagreeing with /api/version (e.g. a stale or duplicate
57965
+ // server is holding the port and reporting a different version), every fresh
57966
+ // page would reload again. That unbounded loop pegs memory and crashes the
57967
+ // browser. Cap reloads to MAX within WINDOW; past that, stop and surface the
57968
+ // mismatch instead of spinning. A genuine update reloads once and then the
57969
+ // versions agree, which clears the counter (see checkServerVersion).
57970
+ var KEY = 'lattice:updateReloads',
57971
+ MAX = 3,
57972
+ WINDOW = 60000,
57973
+ now = Date.now(),
57974
+ recent = [];
57975
+ try {
57976
+ recent = (JSON.parse(sessionStorage.getItem(KEY) || '[]') || []).filter(function (t) {
57977
+ return now - t < WINDOW;
57978
+ });
57979
+ } catch (_) {
57980
+ /* sessionStorage blocked \u2014 degrade to a single best-effort reload below */
57981
+ }
57982
+ if (recent.length >= MAX) {
57983
+ showUpdatePill('Version mismatch \u2014 stopped auto-reloading. Reload manually if needed.');
57984
+ return;
57985
+ }
57986
+ recent.push(now);
57987
+ try {
57988
+ sessionStorage.setItem(KEY, JSON.stringify(recent));
57989
+ } catch (_) {
57990
+ /* best-effort */
57991
+ }
57930
57992
  reloadingForUpdate = true;
57931
57993
  showUpdatePill(label || 'Updated \u2014 reloading\u2026');
57932
- setTimeout(function () { location.reload(); }, 600);
57994
+ setTimeout(function () {
57995
+ location.reload();
57996
+ }, 600);
57933
57997
  }
57934
57998
  // Manual upgrade fallback: show an "Update available \u2014 Upgrade" link next to
57935
57999
  // the version chip only when the server reports a newer, installable version.
@@ -57961,7 +58025,16 @@ var appJs = `
57961
58025
  var v = d && d.version ? String(d.version).replace(/^v/, '').trim() : '';
57962
58026
  if (!v) return;
57963
58027
  if (v !== BOOT_VERSION) reloadForUpdate('Updated to v' + v + ' \u2014 reloading\u2026');
57964
- else hideUpdatePill();
58028
+ else {
58029
+ hideUpdatePill();
58030
+ // Versions agree \u2014 clear the reload-loop guard so a later genuine
58031
+ // update can reload again from a clean slate.
58032
+ try {
58033
+ sessionStorage.removeItem('lattice:updateReloads');
58034
+ } catch (_) {
58035
+ /* best-effort */
58036
+ }
58037
+ }
57965
58038
  })
57966
58039
  .catch(function () { /* offline / mid-restart \u2014 the next reconnect retries */ });
57967
58040
  // Refresh the manual-upgrade link alongside the reconnect version check.
@@ -66322,11 +66395,11 @@ var import_yaml6 = require("yaml");
66322
66395
  init_lattice();
66323
66396
  init_user_config();
66324
66397
  init_cloud_connect();
66325
- var import_node_crypto19 = require("crypto");
66398
+ var import_node_crypto20 = require("crypto");
66326
66399
  init_members();
66327
66400
 
66328
66401
  // src/cloud/invite.ts
66329
- var import_node_crypto18 = require("crypto");
66402
+ var import_node_crypto19 = require("crypto");
66330
66403
  var VERSION = 1;
66331
66404
  var SALT_LEN = 16;
66332
66405
  var SECRET_LEN = 32;
@@ -66344,15 +66417,15 @@ function poolerAwareUser(host, role, ownerUser) {
66344
66417
  return ref ? `${role}.${ref}` : role;
66345
66418
  }
66346
66419
  function deriveKey2(tokenSecret, email, salt) {
66347
- const emailSalt = Buffer.from((0, import_node_crypto18.scryptSync)(normalizeEmail(email), salt, KEY_LEN));
66348
- return Buffer.from((0, import_node_crypto18.hkdfSync)("sha256", tokenSecret, emailSalt, HKDF_INFO, KEY_LEN));
66420
+ const emailSalt = Buffer.from((0, import_node_crypto19.scryptSync)(normalizeEmail(email), salt, KEY_LEN));
66421
+ return Buffer.from((0, import_node_crypto19.hkdfSync)("sha256", tokenSecret, emailSalt, HKDF_INFO, KEY_LEN));
66349
66422
  }
66350
66423
  function mintInviteToken(input) {
66351
66424
  const email = normalizeEmail(input.email);
66352
66425
  if (!email) throw new Error("lattice: an invite must be bound to an email");
66353
- const salt = (0, import_node_crypto18.randomBytes)(SALT_LEN);
66354
- const tokenSecret = (0, import_node_crypto18.randomBytes)(SECRET_LEN);
66355
- const nonce = (0, import_node_crypto18.randomBytes)(NONCE_LEN);
66426
+ const salt = (0, import_node_crypto19.randomBytes)(SALT_LEN);
66427
+ const tokenSecret = (0, import_node_crypto19.randomBytes)(SECRET_LEN);
66428
+ const nonce = (0, import_node_crypto19.randomBytes)(NONCE_LEN);
66356
66429
  const key = deriveKey2(tokenSecret, email, salt);
66357
66430
  const payload = {
66358
66431
  v: 1,
@@ -66366,7 +66439,7 @@ function mintInviteToken(input) {
66366
66439
  expires_at: input.expiresAt.toISOString(),
66367
66440
  ...input.workspaceName?.trim() ? { workspace_name: input.workspaceName.trim() } : {}
66368
66441
  };
66369
- const cipher = (0, import_node_crypto18.createCipheriv)("aes-256-gcm", key, nonce);
66442
+ const cipher = (0, import_node_crypto19.createCipheriv)("aes-256-gcm", key, nonce);
66370
66443
  cipher.setAAD(Buffer.from(email, "utf8"));
66371
66444
  const ct = Buffer.concat([cipher.update(JSON.stringify(payload), "utf8"), cipher.final()]);
66372
66445
  const tag = cipher.getAuthTag();
@@ -66389,7 +66462,7 @@ function redeemInviteToken(email, token) {
66389
66462
  const tag = raw.subarray(raw.length - TAG_LEN2);
66390
66463
  const ct = raw.subarray(off, raw.length - TAG_LEN2);
66391
66464
  const key = deriveKey2(tokenSecret, normEmail, salt);
66392
- const decipher = (0, import_node_crypto18.createDecipheriv)("aes-256-gcm", key, nonce);
66465
+ const decipher = (0, import_node_crypto19.createDecipheriv)("aes-256-gcm", key, nonce);
66393
66466
  decipher.setAAD(Buffer.from(normEmail, "utf8"));
66394
66467
  decipher.setAuthTag(tag);
66395
66468
  let plaintext;
@@ -66417,7 +66490,7 @@ function redeemInviteToken(email, token) {
66417
66490
  init_markdown();
66418
66491
  init_rls();
66419
66492
  init_adapter();
66420
- var import_node_crypto20 = require("crypto");
66493
+ var import_node_crypto21 = require("crypto");
66421
66494
  init_parser();
66422
66495
  init_lattice_root();
66423
66496
  init_workspace();
@@ -66502,7 +66575,7 @@ function parseAndValidateLogo(dataUri) {
66502
66575
  error: `logo must be square (got ${String(dims.width)}\xD7${String(dims.height)})`
66503
66576
  };
66504
66577
  }
66505
- return { ok: true, mime, bytes, etag: (0, import_node_crypto19.createHash)("sha256").update(bytes).digest("hex") };
66578
+ return { ok: true, mime, bytes, etag: (0, import_node_crypto20.createHash)("sha256").update(bytes).digest("hex") };
66506
66579
  }
66507
66580
  function updateActiveWorkspaceToCloud(configPath, displayName, key) {
66508
66581
  const root6 = findLatticeRoot((0, import_node_path36.dirname)(configPath));
@@ -67012,7 +67085,7 @@ async function dispatchDbConfigRoute(req, res, ctx) {
67012
67085
  `INSERT INTO "__lattice_member_invites" ("id","role","email_hash","email","expires_at")
67013
67086
  VALUES (?, ?, ?, ?, ?)`,
67014
67087
  [
67015
- (0, import_node_crypto20.randomUUID)(),
67088
+ (0, import_node_crypto21.randomUUID)(),
67016
67089
  role,
67017
67090
  emailHash,
67018
67091
  // Plaintext email stored ONLY in this owner-only table so the owner's
@@ -67838,7 +67911,7 @@ var import_node_os9 = require("os");
67838
67911
  var import_node_path37 = require("path");
67839
67912
  init_mutations();
67840
67913
  init_extract();
67841
- var import_node_crypto21 = require("crypto");
67914
+ var import_node_crypto22 = require("crypto");
67842
67915
  init_assistant_routes();
67843
67916
  init_enrich();
67844
67917
  init_ingest_url();
@@ -68079,7 +68152,7 @@ async function dispatchIngestRoute(req, res, ctx) {
68079
68152
  let s3Status = null;
68080
68153
  const s3cfg = resolveActiveS3Config(ctx.configPath);
68081
68154
  if (s3cfg) {
68082
- const sha256 = blob?.sha256 ?? (0, import_node_crypto21.createHash)("sha256").update(buf).digest("hex");
68155
+ const sha256 = blob?.sha256 ?? (0, import_node_crypto22.createHash)("sha256").update(buf).digest("hex");
68083
68156
  const key = s3Key(s3cfg.prefix, sha256);
68084
68157
  try {
68085
68158
  const store = await createS3Store(s3cfg);
@@ -68111,7 +68184,7 @@ async function dispatchIngestRoute(req, res, ctx) {
68111
68184
  realPath = rawFilePath;
68112
68185
  }
68113
68186
  }
68114
- const fileSha = blob?.sha256 ?? s3Ref?.sha256 ?? (0, import_node_crypto21.createHash)("sha256").update(buf).digest("hex");
68187
+ const fileSha = blob?.sha256 ?? s3Ref?.sha256 ?? (0, import_node_crypto22.createHash)("sha256").update(buf).digest("hex");
68115
68188
  const uploadRow = {
68116
68189
  id: fileId,
68117
68190
  ...fileIdentity(name2, fileId),
@@ -71200,7 +71273,7 @@ ${e6.stack ?? ""}`
71200
71273
  // src/cloud/file-source-key-store.ts
71201
71274
  var import_node_fs36 = require("fs");
71202
71275
  var import_node_path39 = require("path");
71203
- var import_node_crypto22 = require("crypto");
71276
+ var import_node_crypto23 = require("crypto");
71204
71277
  var ENC_HEADER = "LATTICE-KMS-v1\n";
71205
71278
  var SCRYPT_N = 1 << 15;
71206
71279
  var SCRYPT_R = 8;
@@ -71227,7 +71300,7 @@ var FileSourceKeyStore = class {
71227
71300
  getOrCreate(sourceId) {
71228
71301
  let key = this.cache.get(sourceId);
71229
71302
  if (!key) {
71230
- key = (0, import_node_crypto22.randomBytes)(KEY_LEN2);
71303
+ key = (0, import_node_crypto23.randomBytes)(KEY_LEN2);
71231
71304
  this.cache.set(sourceId, key);
71232
71305
  this.persist();
71233
71306
  }
@@ -71271,7 +71344,7 @@ var FileSourceKeyStore = class {
71271
71344
  const obj2 = {};
71272
71345
  for (const [k6, v2] of this.cache) obj2[k6] = v2.toString("base64");
71273
71346
  const encoded = this.encodeFile(obj2);
71274
- const tmpPath = `${this.path}.tmp-${process.pid.toString()}-${(0, import_node_crypto22.randomBytes)(4).toString("hex")}`;
71347
+ const tmpPath = `${this.path}.tmp-${process.pid.toString()}-${(0, import_node_crypto23.randomBytes)(4).toString("hex")}`;
71275
71348
  (0, import_node_fs36.writeFileSync)(tmpPath, encoded, { mode: 384 });
71276
71349
  try {
71277
71350
  (0, import_node_fs36.chmodSync)(tmpPath, 384);
@@ -71314,14 +71387,14 @@ var FileSourceKeyStore = class {
71314
71387
  if (passphrase === void 0) {
71315
71388
  throw new Error("lattice: key file is encrypted but no passphrase was configured");
71316
71389
  }
71317
- const derived = (0, import_node_crypto22.scryptSync)(passphrase, salt, KEY_LEN2, {
71390
+ const derived = (0, import_node_crypto23.scryptSync)(passphrase, salt, KEY_LEN2, {
71318
71391
  N: SCRYPT_N,
71319
71392
  r: SCRYPT_R,
71320
71393
  p: SCRYPT_P,
71321
71394
  maxmem: 64 * 1024 * 1024
71322
71395
  // raise Node's default 32MB cap so N=2^15 fits
71323
71396
  });
71324
- const decipher = (0, import_node_crypto22.createDecipheriv)("aes-256-gcm", derived, iv);
71397
+ const decipher = (0, import_node_crypto23.createDecipheriv)("aes-256-gcm", derived, iv);
71325
71398
  decipher.setAuthTag(tag);
71326
71399
  let plaintext;
71327
71400
  try {
@@ -71336,15 +71409,15 @@ var FileSourceKeyStore = class {
71336
71409
  if (!this.passphrase) {
71337
71410
  return Buffer.from(json, "utf8");
71338
71411
  }
71339
- const salt = (0, import_node_crypto22.randomBytes)(SALT_LEN2);
71340
- const iv = (0, import_node_crypto22.randomBytes)(IV_LEN2);
71341
- const derived = (0, import_node_crypto22.scryptSync)(this.passphrase, salt, KEY_LEN2, {
71412
+ const salt = (0, import_node_crypto23.randomBytes)(SALT_LEN2);
71413
+ const iv = (0, import_node_crypto23.randomBytes)(IV_LEN2);
71414
+ const derived = (0, import_node_crypto23.scryptSync)(this.passphrase, salt, KEY_LEN2, {
71342
71415
  N: SCRYPT_N,
71343
71416
  r: SCRYPT_R,
71344
71417
  p: SCRYPT_P,
71345
71418
  maxmem: 64 * 1024 * 1024
71346
71419
  });
71347
- const cipher = (0, import_node_crypto22.createCipheriv)("aes-256-gcm", derived, iv);
71420
+ const cipher = (0, import_node_crypto23.createCipheriv)("aes-256-gcm", derived, iv);
71348
71421
  const ct = Buffer.concat([cipher.update(json, "utf8"), cipher.final()]);
71349
71422
  const tag = cipher.getAuthTag();
71350
71423
  const body = `${salt.toString("hex")}:${iv.toString("hex")}:${Buffer.concat([ct, tag]).toString("hex")}`;
package/dist/index.js CHANGED
@@ -50597,6 +50597,21 @@ var init_registry = __esm({
50597
50597
  ["table", "values"]
50598
50598
  )
50599
50599
  },
50600
+ {
50601
+ name: "create_secret",
50602
+ description: "Store a secret/credential \u2014 an API key, password, OAuth token, connection string, etc. \u2014 by name in the encrypted secrets store. Use this whenever the user gives you a credential to save or asks you to remember/store a secret. WRITE-ONLY: you can save a secret but you can NEVER read, list, echo, or retrieve existing secret values \u2014 they are hidden from you. The value is encrypted at rest.",
50603
+ mutates: true,
50604
+ category: "row",
50605
+ args: obj(
50606
+ {
50607
+ name: str('A short label for the secret, e.g. "GitHub password" or "OpenAI API key".'),
50608
+ value: str("The secret value to store."),
50609
+ kind: str('Optional kind, e.g. "password", "api_key", "token", "connection_string".'),
50610
+ description: str("Optional note about what the secret is for.")
50611
+ },
50612
+ ["name", "value"]
50613
+ )
50614
+ },
50600
50615
  {
50601
50616
  name: "create_artifact",
50602
50617
  description: "Create a markdown document and save it as a file artifact. Use this whenever the user asks you to create, write, draft, or make a document, note, write-up, summary, report, or file \u2014 you author the content as GitHub-flavored markdown and it is saved in the files entity as a markdown artifact, then opened in the viewer for them. Prefer this over create_row on files for any document the user wants to keep. It follows the same sharing rules as any file (private mode \u2192 private).",
@@ -52803,6 +52818,7 @@ var init_ingest_url = __esm({
52803
52818
  });
52804
52819
 
52805
52820
  // src/gui/ai/dispatch.ts
52821
+ import { randomUUID } from "crypto";
52806
52822
  function visibilityDenialReason(opts) {
52807
52823
  if (opts.kind === "table") {
52808
52824
  return opts.canManageTableDefault ? null : "Only the workspace owner can change a table's default sharing.";
@@ -53018,6 +53034,21 @@ async function executeFunction(ctx, name, args) {
53018
53034
  );
53019
53035
  return { ok: true, result: { id } };
53020
53036
  }
53037
+ case "create_secret": {
53038
+ const secretName = requireString(args.name, "name");
53039
+ const secretValue2 = requireString(args.value, "value");
53040
+ const kind = typeof args.kind === "string" && args.kind ? args.kind : null;
53041
+ const description = typeof args.description === "string" && args.description ? args.description : null;
53042
+ const id = randomUUID();
53043
+ await ctx.db.insert("secrets", {
53044
+ id,
53045
+ name: secretName,
53046
+ value: secretValue2,
53047
+ kind,
53048
+ description
53049
+ });
53050
+ return { ok: true, result: { id, name: secretName } };
53051
+ }
53021
53052
  case "create_artifact": {
53022
53053
  const table = requireTable("files", ctx.validTables);
53023
53054
  const title = requireString(args.title, "title");
@@ -53348,6 +53379,7 @@ var init_dispatch = __esm({
53348
53379
  "get_history",
53349
53380
  "create_row",
53350
53381
  "create_artifact",
53382
+ "create_secret",
53351
53383
  "ingest_url",
53352
53384
  "set_definition",
53353
53385
  "set_visibility",
@@ -57752,9 +57784,41 @@ var appJs = `
57752
57784
  }
57753
57785
  function reloadForUpdate(label) {
57754
57786
  if (reloadingForUpdate) return;
57787
+ // Guard against an infinite reload loop. Reloading when the server version
57788
+ // changes is the seamless-update trigger \u2014 but if the version the page was
57789
+ // SERVED with keeps disagreeing with /api/version (e.g. a stale or duplicate
57790
+ // server is holding the port and reporting a different version), every fresh
57791
+ // page would reload again. That unbounded loop pegs memory and crashes the
57792
+ // browser. Cap reloads to MAX within WINDOW; past that, stop and surface the
57793
+ // mismatch instead of spinning. A genuine update reloads once and then the
57794
+ // versions agree, which clears the counter (see checkServerVersion).
57795
+ var KEY = 'lattice:updateReloads',
57796
+ MAX = 3,
57797
+ WINDOW = 60000,
57798
+ now = Date.now(),
57799
+ recent = [];
57800
+ try {
57801
+ recent = (JSON.parse(sessionStorage.getItem(KEY) || '[]') || []).filter(function (t) {
57802
+ return now - t < WINDOW;
57803
+ });
57804
+ } catch (_) {
57805
+ /* sessionStorage blocked \u2014 degrade to a single best-effort reload below */
57806
+ }
57807
+ if (recent.length >= MAX) {
57808
+ showUpdatePill('Version mismatch \u2014 stopped auto-reloading. Reload manually if needed.');
57809
+ return;
57810
+ }
57811
+ recent.push(now);
57812
+ try {
57813
+ sessionStorage.setItem(KEY, JSON.stringify(recent));
57814
+ } catch (_) {
57815
+ /* best-effort */
57816
+ }
57755
57817
  reloadingForUpdate = true;
57756
57818
  showUpdatePill(label || 'Updated \u2014 reloading\u2026');
57757
- setTimeout(function () { location.reload(); }, 600);
57819
+ setTimeout(function () {
57820
+ location.reload();
57821
+ }, 600);
57758
57822
  }
57759
57823
  // Manual upgrade fallback: show an "Update available \u2014 Upgrade" link next to
57760
57824
  // the version chip only when the server reports a newer, installable version.
@@ -57786,7 +57850,16 @@ var appJs = `
57786
57850
  var v = d && d.version ? String(d.version).replace(/^v/, '').trim() : '';
57787
57851
  if (!v) return;
57788
57852
  if (v !== BOOT_VERSION) reloadForUpdate('Updated to v' + v + ' \u2014 reloading\u2026');
57789
- else hideUpdatePill();
57853
+ else {
57854
+ hideUpdatePill();
57855
+ // Versions agree \u2014 clear the reload-loop guard so a later genuine
57856
+ // update can reload again from a clean slate.
57857
+ try {
57858
+ sessionStorage.removeItem('lattice:updateReloads');
57859
+ } catch (_) {
57860
+ /* best-effort */
57861
+ }
57862
+ }
57790
57863
  })
57791
57864
  .catch(function () { /* offline / mid-restart \u2014 the next reconnect retries */ });
57792
57865
  // Refresh the manual-upgrade link alongside the reconnect version check.
@@ -66241,7 +66314,7 @@ function redeemInviteToken(email, token) {
66241
66314
  init_markdown();
66242
66315
  init_rls();
66243
66316
  init_adapter();
66244
- import { randomUUID } from "crypto";
66317
+ import { randomUUID as randomUUID2 } from "crypto";
66245
66318
  init_parser();
66246
66319
  init_lattice_root();
66247
66320
  init_workspace();
@@ -66836,7 +66909,7 @@ async function dispatchDbConfigRoute(req, res, ctx) {
66836
66909
  `INSERT INTO "__lattice_member_invites" ("id","role","email_hash","email","expires_at")
66837
66910
  VALUES (?, ?, ?, ?, ?)`,
66838
66911
  [
66839
- randomUUID(),
66912
+ randomUUID2(),
66840
66913
  role,
66841
66914
  emailHash,
66842
66915
  // Plaintext email stored ONLY in this owner-only table so the owner's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latticesql",
3
- "version": "3.4.5",
3
+ "version": "3.4.7",
4
4
  "description": "Persistent structured memory for AI agent systems — pluggable SQLite or Postgres backend, LLM context bridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",