latticesql 3.4.4 → 3.4.6
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 +156 -35
- package/dist/index.cjs +179 -57
- package/dist/index.d.cts +38 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +155 -34
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -632,14 +632,6 @@ function resolveDbPath(raw, configDir2) {
|
|
|
632
632
|
}
|
|
633
633
|
return resolve(configDir2, raw);
|
|
634
634
|
}
|
|
635
|
-
function warnDeprecatedRef(entity, field, target) {
|
|
636
|
-
const key = `${entity}.${field}`;
|
|
637
|
-
if (warnedDeprecatedRefs.has(key)) return;
|
|
638
|
-
warnedDeprecatedRefs.add(key);
|
|
639
|
-
console.warn(
|
|
640
|
-
`Lattice: one-to-many \`ref:\` on "${entity}.${field}" \u2192 "${target}" is deprecated in favor of many-to-many junction tables and will be removed in 2.0.`
|
|
641
|
-
);
|
|
642
|
-
}
|
|
643
635
|
function entityToTableDef(entityName, entity) {
|
|
644
636
|
const rawFields = entity.fields;
|
|
645
637
|
if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
|
|
@@ -666,7 +658,6 @@ function entityToTableDef(entityName, entity) {
|
|
|
666
658
|
table: field.ref,
|
|
667
659
|
foreignKey: fieldName
|
|
668
660
|
};
|
|
669
|
-
warnDeprecatedRef(entityName, fieldName, field.ref);
|
|
670
661
|
}
|
|
671
662
|
}
|
|
672
663
|
const primaryKey = entity.primaryKey ?? pkFromField;
|
|
@@ -823,12 +814,10 @@ function parseEntityContexts(entityContexts) {
|
|
|
823
814
|
}
|
|
824
815
|
return result;
|
|
825
816
|
}
|
|
826
|
-
var warnedDeprecatedRefs;
|
|
827
817
|
var init_parser = __esm({
|
|
828
818
|
"src/config/parser.ts"() {
|
|
829
819
|
"use strict";
|
|
830
820
|
init_user_config();
|
|
831
|
-
warnedDeprecatedRefs = /* @__PURE__ */ new Set();
|
|
832
821
|
}
|
|
833
822
|
});
|
|
834
823
|
|
|
@@ -10324,6 +10313,21 @@ var init_registry = __esm({
|
|
|
10324
10313
|
["table", "values"]
|
|
10325
10314
|
)
|
|
10326
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
|
+
},
|
|
10327
10331
|
{
|
|
10328
10332
|
name: "create_artifact",
|
|
10329
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).",
|
|
@@ -13167,6 +13171,7 @@ var init_ingest_url = __esm({
|
|
|
13167
13171
|
});
|
|
13168
13172
|
|
|
13169
13173
|
// src/gui/ai/dispatch.ts
|
|
13174
|
+
import { randomUUID } from "crypto";
|
|
13170
13175
|
function visibilityDenialReason(opts) {
|
|
13171
13176
|
if (opts.kind === "table") {
|
|
13172
13177
|
return opts.canManageTableDefault ? null : "Only the workspace owner can change a table's default sharing.";
|
|
@@ -13382,6 +13387,21 @@ async function executeFunction(ctx, name, args) {
|
|
|
13382
13387
|
);
|
|
13383
13388
|
return { ok: true, result: { id } };
|
|
13384
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
|
+
}
|
|
13385
13405
|
case "create_artifact": {
|
|
13386
13406
|
const table = requireTable("files", ctx.validTables);
|
|
13387
13407
|
const title = requireString(args.title, "title");
|
|
@@ -13712,6 +13732,7 @@ var init_dispatch = __esm({
|
|
|
13712
13732
|
"get_history",
|
|
13713
13733
|
"create_row",
|
|
13714
13734
|
"create_artifact",
|
|
13735
|
+
"create_secret",
|
|
13715
13736
|
"ingest_url",
|
|
13716
13737
|
"set_definition",
|
|
13717
13738
|
"set_visibility",
|
|
@@ -54094,6 +54115,8 @@ var css = `
|
|
|
54094
54115
|
.app-version:empty { display: none; }
|
|
54095
54116
|
.app-update { flex: 0 0 auto; color: var(--accent, #4a9); font-size: 12px; white-space: nowrap; }
|
|
54096
54117
|
.app-update[hidden] { display: none; }
|
|
54118
|
+
#app-update-link { flex: 0 0 auto; margin-left: 8px; color: var(--accent, #4a9); font-size: 12px; cursor: pointer; white-space: nowrap; }
|
|
54119
|
+
#app-update-link[hidden] { display: none; }
|
|
54097
54120
|
/* Unseen-change count next to a sidebar entity. */
|
|
54098
54121
|
.nav-badge {
|
|
54099
54122
|
display: inline-block; min-width: 16px; text-align: center;
|
|
@@ -55572,6 +55595,12 @@ var appJs = `
|
|
|
55572
55595
|
// drag handle once the app has booted.
|
|
55573
55596
|
var savedRail = parseInt(window.localStorage.getItem(RAIL_KEY) || '', 10);
|
|
55574
55597
|
if (!isNaN(savedRail)) applyRailWidth(savedRail);
|
|
55598
|
+
// The version chip + manual-upgrade link live in the static shell (present
|
|
55599
|
+
// from first paint, in both the normal and virgin-state boots), so wire the
|
|
55600
|
+
// click handler and run the first availability check here \u2014 independent of
|
|
55601
|
+
// the async workspace bootstrap. checkServerVersion() refreshes it later.
|
|
55602
|
+
wireUpdateLink();
|
|
55603
|
+
checkUpdateAvailable();
|
|
55575
55604
|
// Failsafe: never leave the overlay up forever if a fetch hangs without
|
|
55576
55605
|
// rejecting, or a future early-return (e.g. the virgin-state screen)
|
|
55577
55606
|
// bypasses the .then() tail. Idempotent, so a later real hide is a no-op.
|
|
@@ -56056,6 +56085,26 @@ var appJs = `
|
|
|
56056
56085
|
showUpdatePill(label || 'Updated \u2014 reloading\u2026');
|
|
56057
56086
|
setTimeout(function () { location.reload(); }, 600);
|
|
56058
56087
|
}
|
|
56088
|
+
// Manual upgrade fallback: show an "Update available \u2014 Upgrade" link next to
|
|
56089
|
+
// the version chip only when the server reports a newer, installable version.
|
|
56090
|
+
// The auto-updater installs in the background on its own cadence; this lets
|
|
56091
|
+
// the user force it now. Best-effort; the link stays hidden on any failure.
|
|
56092
|
+
function checkUpdateAvailable() {
|
|
56093
|
+
var el = document.getElementById('app-update-link');
|
|
56094
|
+
if (!el) return;
|
|
56095
|
+
fetch('/api/update/status')
|
|
56096
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
56097
|
+
.then(function (s) {
|
|
56098
|
+
if (s && s.latest && s.current && s.latest !== s.current && s.installable) {
|
|
56099
|
+
el.textContent = 'Update available \u2014 Upgrade';
|
|
56100
|
+
el.title = 'Install v' + s.latest + ' and restart';
|
|
56101
|
+
el.hidden = false;
|
|
56102
|
+
} else {
|
|
56103
|
+
el.hidden = true;
|
|
56104
|
+
}
|
|
56105
|
+
})
|
|
56106
|
+
.catch(function () { /* best-effort \u2014 keep the link hidden */ });
|
|
56107
|
+
}
|
|
56059
56108
|
// On every (re)connect, ask the server its version. A change vs BOOT_VERSION
|
|
56060
56109
|
// means a relaunch onto new code \u2192 reload. Best-effort; never throws.
|
|
56061
56110
|
function checkServerVersion() {
|
|
@@ -56069,6 +56118,31 @@ var appJs = `
|
|
|
56069
56118
|
else hideUpdatePill();
|
|
56070
56119
|
})
|
|
56071
56120
|
.catch(function () { /* offline / mid-restart \u2014 the next reconnect retries */ });
|
|
56121
|
+
// Refresh the manual-upgrade link alongside the reconnect version check.
|
|
56122
|
+
checkUpdateAvailable();
|
|
56123
|
+
}
|
|
56124
|
+
// Wire the manual-upgrade link's click: kick off the install (the server
|
|
56125
|
+
// installs the latest and restarts onto it) and surface the progress. On
|
|
56126
|
+
// success we do nothing else \u2014 the update-applied event + the reconnect
|
|
56127
|
+
// version check land the page on the new version (no manual reload). A
|
|
56128
|
+
// false ok means the install can't run (unsupervised) \u2014 toast why.
|
|
56129
|
+
function wireUpdateLink() {
|
|
56130
|
+
var el = document.getElementById('app-update-link');
|
|
56131
|
+
if (!el) return;
|
|
56132
|
+
el.addEventListener('click', function (e) {
|
|
56133
|
+
e.preventDefault();
|
|
56134
|
+
el.hidden = true;
|
|
56135
|
+
showUpdatePill('Updating\u2026');
|
|
56136
|
+
fetch('/api/update/apply', { method: 'POST' })
|
|
56137
|
+
.then(function (r) { return r.json(); })
|
|
56138
|
+
.then(function (d) {
|
|
56139
|
+
if (d && d.ok === false) {
|
|
56140
|
+
hideUpdatePill();
|
|
56141
|
+
showToast(d.error || 'Update unavailable', {});
|
|
56142
|
+
}
|
|
56143
|
+
})
|
|
56144
|
+
.catch(function () { /* server may already be restarting */ });
|
|
56145
|
+
});
|
|
56072
56146
|
}
|
|
56073
56147
|
function dispatchStreamMessage(type, data) {
|
|
56074
56148
|
if (type === 'realtime-state') {
|
|
@@ -62886,6 +62960,7 @@ var guiAppHtml = `<!doctype html>
|
|
|
62886
62960
|
<span class="offline-pill" id="offline-pill" title="Edits queued offline \u2014 will sync when the cloud reconnects" hidden></span>
|
|
62887
62961
|
<span class="app-update" id="app-update" title="A new version is being applied" hidden></span>
|
|
62888
62962
|
<span class="app-version" id="app-version" title="Lattice version"><!--LATTICE_VERSION--></span>
|
|
62963
|
+
<a id="app-update-link" href="#" hidden>Update available \u2014 Upgrade</a>
|
|
62889
62964
|
<button id="settings-gear" title="Settings" aria-label="Open settings">
|
|
62890
62965
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
62891
62966
|
<circle cx="12" cy="12" r="3"/>
|
|
@@ -64809,7 +64884,7 @@ function redeemInviteToken(email, token) {
|
|
|
64809
64884
|
init_markdown();
|
|
64810
64885
|
init_rls();
|
|
64811
64886
|
init_adapter();
|
|
64812
|
-
import { randomUUID } from "crypto";
|
|
64887
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
64813
64888
|
|
|
64814
64889
|
// src/framework/cloud-migration.ts
|
|
64815
64890
|
init_lattice();
|
|
@@ -65604,7 +65679,7 @@ async function dispatchDbConfigRoute(req, res, ctx) {
|
|
|
65604
65679
|
`INSERT INTO "__lattice_member_invites" ("id","role","email_hash","email","expires_at")
|
|
65605
65680
|
VALUES (?, ?, ?, ?, ?)`,
|
|
65606
65681
|
[
|
|
65607
|
-
|
|
65682
|
+
randomUUID2(),
|
|
65608
65683
|
role,
|
|
65609
65684
|
emailHash,
|
|
65610
65685
|
// Plaintext email stored ONLY in this owner-only table so the owner's
|
|
@@ -68096,6 +68171,28 @@ async function startGuiServer(options) {
|
|
|
68096
68171
|
setActive(next, created.id);
|
|
68097
68172
|
return created.id;
|
|
68098
68173
|
};
|
|
68174
|
+
const cleanupWorkspaceFiles = (root6, ws) => {
|
|
68175
|
+
if (!ws.configPath && ws.kind === "local") {
|
|
68176
|
+
rmSync(workspaceDir(root6, ws.dir), { recursive: true, force: true });
|
|
68177
|
+
} else if (ws.kind === "cloud") {
|
|
68178
|
+
if (ws.configPath && existsSync25(ws.configPath)) {
|
|
68179
|
+
rmSync(ws.configPath, { force: true });
|
|
68180
|
+
}
|
|
68181
|
+
const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
|
|
68182
|
+
const label = labelMatch?.[1];
|
|
68183
|
+
if (label) {
|
|
68184
|
+
const stillUsed = listWorkspaces(root6).some(
|
|
68185
|
+
(w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
|
|
68186
|
+
);
|
|
68187
|
+
if (!stillUsed) {
|
|
68188
|
+
try {
|
|
68189
|
+
deleteDbCredential(label);
|
|
68190
|
+
} catch {
|
|
68191
|
+
}
|
|
68192
|
+
}
|
|
68193
|
+
}
|
|
68194
|
+
}
|
|
68195
|
+
};
|
|
68099
68196
|
const handleVirginRoute = async (req, res, pathname, method) => {
|
|
68100
68197
|
if (method === "GET" && pathname === "/") {
|
|
68101
68198
|
sendText(
|
|
@@ -68147,6 +68244,35 @@ async function startGuiServer(options) {
|
|
|
68147
68244
|
}
|
|
68148
68245
|
return true;
|
|
68149
68246
|
}
|
|
68247
|
+
if (method === "POST" && pathname === "/api/workspaces/delete") {
|
|
68248
|
+
if (!latticeRoot) {
|
|
68249
|
+
sendJson(res, { error: "No .lattice root \u2014 workspaces unavailable" }, 400);
|
|
68250
|
+
return true;
|
|
68251
|
+
}
|
|
68252
|
+
const body = await readJson(req);
|
|
68253
|
+
if (typeof body.id !== "string") {
|
|
68254
|
+
sendJson(res, { error: "id must be a string" }, 400);
|
|
68255
|
+
return true;
|
|
68256
|
+
}
|
|
68257
|
+
const ws = getWorkspace(latticeRoot, body.id);
|
|
68258
|
+
if (!ws) {
|
|
68259
|
+
sendJson(res, { error: `No workspace with id ${body.id}` }, 400);
|
|
68260
|
+
return true;
|
|
68261
|
+
}
|
|
68262
|
+
removeWorkspace(latticeRoot, ws.id);
|
|
68263
|
+
try {
|
|
68264
|
+
cleanupWorkspaceFiles(latticeRoot, ws);
|
|
68265
|
+
} catch (e6) {
|
|
68266
|
+
sendJson(
|
|
68267
|
+
res,
|
|
68268
|
+
{ error: `Workspace unregistered but file cleanup failed: ${e6.message}` },
|
|
68269
|
+
500
|
|
68270
|
+
);
|
|
68271
|
+
return true;
|
|
68272
|
+
}
|
|
68273
|
+
sendJson(res, { ok: true, switchedTo: null });
|
|
68274
|
+
return true;
|
|
68275
|
+
}
|
|
68150
68276
|
if (method === "POST" && pathname === "/api/cloud/redeem-invite") {
|
|
68151
68277
|
await redeemInvite(createCloudWorkspace, req, res);
|
|
68152
68278
|
return true;
|
|
@@ -68181,6 +68307,18 @@ async function startGuiServer(options) {
|
|
|
68181
68307
|
);
|
|
68182
68308
|
return;
|
|
68183
68309
|
}
|
|
68310
|
+
if (method === "POST" && pathname === "/api/update/apply") {
|
|
68311
|
+
if (updateService) {
|
|
68312
|
+
void updateService.checkNow(true);
|
|
68313
|
+
sendJson(res, { ok: true, status: updateService.status() });
|
|
68314
|
+
} else {
|
|
68315
|
+
sendJson(res, {
|
|
68316
|
+
ok: false,
|
|
68317
|
+
error: "Automatic update is not available for this install. Reinstall from https://latticesql.com to get the latest version."
|
|
68318
|
+
});
|
|
68319
|
+
}
|
|
68320
|
+
return;
|
|
68321
|
+
}
|
|
68184
68322
|
if (!activeRef) {
|
|
68185
68323
|
if (await handleVirginRoute(req, res, pathname, method)) return;
|
|
68186
68324
|
sendJson(res, { error: "No active workspace" }, 409);
|
|
@@ -69274,26 +69412,7 @@ async function startGuiServer(options) {
|
|
|
69274
69412
|
}
|
|
69275
69413
|
removeWorkspace(latticeRoot, ws.id);
|
|
69276
69414
|
try {
|
|
69277
|
-
|
|
69278
|
-
rmSync(workspaceDir(latticeRoot, ws.dir), { recursive: true, force: true });
|
|
69279
|
-
} else if (ws.kind === "cloud") {
|
|
69280
|
-
if (ws.configPath && existsSync25(ws.configPath)) {
|
|
69281
|
-
rmSync(ws.configPath, { force: true });
|
|
69282
|
-
}
|
|
69283
|
-
const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
|
|
69284
|
-
const label = labelMatch?.[1];
|
|
69285
|
-
if (label) {
|
|
69286
|
-
const stillUsed = listWorkspaces(latticeRoot).some(
|
|
69287
|
-
(w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
|
|
69288
|
-
);
|
|
69289
|
-
if (!stillUsed) {
|
|
69290
|
-
try {
|
|
69291
|
-
deleteDbCredential(label);
|
|
69292
|
-
} catch {
|
|
69293
|
-
}
|
|
69294
|
-
}
|
|
69295
|
-
}
|
|
69296
|
-
}
|
|
69415
|
+
cleanupWorkspaceFiles(latticeRoot, ws);
|
|
69297
69416
|
} catch (e6) {
|
|
69298
69417
|
sendJson(
|
|
69299
69418
|
res,
|
|
@@ -69759,7 +69878,9 @@ ${e6.stack ?? ""}`
|
|
|
69759
69878
|
}
|
|
69760
69879
|
}
|
|
69761
69880
|
};
|
|
69762
|
-
if (options.
|
|
69881
|
+
if (options.updateServiceFactory) {
|
|
69882
|
+
updateService = options.updateServiceFactory(broadcast);
|
|
69883
|
+
} else if (options.selfUpdate && guiVersion) {
|
|
69763
69884
|
updateService = createUpdateService({ currentVersion: guiVersion, emit: broadcast });
|
|
69764
69885
|
}
|
|
69765
69886
|
const handleEventStream = (ws) => {
|
|
@@ -70132,7 +70253,7 @@ function printHelp() {
|
|
|
70132
70253
|
);
|
|
70133
70254
|
}
|
|
70134
70255
|
function getVersion() {
|
|
70135
|
-
if (true) return "3.4.
|
|
70256
|
+
if (true) return "3.4.6";
|
|
70136
70257
|
try {
|
|
70137
70258
|
const pkgPath = new URL("../package.json", import.meta.url).pathname;
|
|
70138
70259
|
const pkg = JSON.parse(readFileSync20(pkgPath, "utf-8"));
|
package/dist/index.cjs
CHANGED
|
@@ -4527,14 +4527,6 @@ function resolveDbPath(raw, configDir2) {
|
|
|
4527
4527
|
}
|
|
4528
4528
|
return (0, import_node_path11.resolve)(configDir2, raw);
|
|
4529
4529
|
}
|
|
4530
|
-
function warnDeprecatedRef(entity, field, target) {
|
|
4531
|
-
const key = `${entity}.${field}`;
|
|
4532
|
-
if (warnedDeprecatedRefs.has(key)) return;
|
|
4533
|
-
warnedDeprecatedRefs.add(key);
|
|
4534
|
-
console.warn(
|
|
4535
|
-
`Lattice: one-to-many \`ref:\` on "${entity}.${field}" \u2192 "${target}" is deprecated in favor of many-to-many junction tables and will be removed in 2.0.`
|
|
4536
|
-
);
|
|
4537
|
-
}
|
|
4538
4530
|
function entityToTableDef(entityName, entity) {
|
|
4539
4531
|
const rawFields = entity.fields;
|
|
4540
4532
|
if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
|
|
@@ -4561,7 +4553,6 @@ function entityToTableDef(entityName, entity) {
|
|
|
4561
4553
|
table: field.ref,
|
|
4562
4554
|
foreignKey: fieldName
|
|
4563
4555
|
};
|
|
4564
|
-
warnDeprecatedRef(entityName, fieldName, field.ref);
|
|
4565
4556
|
}
|
|
4566
4557
|
}
|
|
4567
4558
|
const primaryKey = entity.primaryKey ?? pkFromField;
|
|
@@ -4718,7 +4709,7 @@ function parseEntityContexts(entityContexts) {
|
|
|
4718
4709
|
}
|
|
4719
4710
|
return result;
|
|
4720
4711
|
}
|
|
4721
|
-
var import_node_fs10, import_node_path11, import_yaml3
|
|
4712
|
+
var import_node_fs10, import_node_path11, import_yaml3;
|
|
4722
4713
|
var init_parser = __esm({
|
|
4723
4714
|
"src/config/parser.ts"() {
|
|
4724
4715
|
"use strict";
|
|
@@ -4726,7 +4717,6 @@ var init_parser = __esm({
|
|
|
4726
4717
|
import_node_path11 = require("path");
|
|
4727
4718
|
import_yaml3 = require("yaml");
|
|
4728
4719
|
init_user_config();
|
|
4729
|
-
warnedDeprecatedRefs = /* @__PURE__ */ new Set();
|
|
4730
4720
|
}
|
|
4731
4721
|
});
|
|
4732
4722
|
|
|
@@ -50616,6 +50606,21 @@ var init_registry = __esm({
|
|
|
50616
50606
|
["table", "values"]
|
|
50617
50607
|
)
|
|
50618
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
|
+
},
|
|
50619
50624
|
{
|
|
50620
50625
|
name: "create_artifact",
|
|
50621
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).",
|
|
@@ -53038,6 +53043,21 @@ async function executeFunction(ctx, name, args) {
|
|
|
53038
53043
|
);
|
|
53039
53044
|
return { ok: true, result: { id } };
|
|
53040
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
|
+
}
|
|
53041
53061
|
case "create_artifact": {
|
|
53042
53062
|
const table = requireTable("files", ctx.validTables);
|
|
53043
53063
|
const title = requireString(args.title, "title");
|
|
@@ -53341,10 +53361,11 @@ async function executeFunction(ctx, name, args) {
|
|
|
53341
53361
|
return { ok: false, error: e6.message };
|
|
53342
53362
|
}
|
|
53343
53363
|
}
|
|
53344
|
-
var DISPATCHABLE, ASSISTANT_HIDDEN_TABLES, SECRET_MASK, BULK_FILTER_OPS;
|
|
53364
|
+
var import_node_crypto18, DISPATCHABLE, ASSISTANT_HIDDEN_TABLES, SECRET_MASK, BULK_FILTER_OPS;
|
|
53345
53365
|
var init_dispatch = __esm({
|
|
53346
53366
|
"src/gui/ai/dispatch.ts"() {
|
|
53347
53367
|
"use strict";
|
|
53368
|
+
import_node_crypto18 = require("crypto");
|
|
53348
53369
|
init_registry();
|
|
53349
53370
|
init_lattice_docs();
|
|
53350
53371
|
init_fts();
|
|
@@ -53368,6 +53389,7 @@ var init_dispatch = __esm({
|
|
|
53368
53389
|
"get_history",
|
|
53369
53390
|
"create_row",
|
|
53370
53391
|
"create_artifact",
|
|
53392
|
+
"create_secret",
|
|
53371
53393
|
"ingest_url",
|
|
53372
53394
|
"set_definition",
|
|
53373
53395
|
"set_visibility",
|
|
@@ -55971,6 +55993,8 @@ var css = `
|
|
|
55971
55993
|
.app-version:empty { display: none; }
|
|
55972
55994
|
.app-update { flex: 0 0 auto; color: var(--accent, #4a9); font-size: 12px; white-space: nowrap; }
|
|
55973
55995
|
.app-update[hidden] { display: none; }
|
|
55996
|
+
#app-update-link { flex: 0 0 auto; margin-left: 8px; color: var(--accent, #4a9); font-size: 12px; cursor: pointer; white-space: nowrap; }
|
|
55997
|
+
#app-update-link[hidden] { display: none; }
|
|
55974
55998
|
/* Unseen-change count next to a sidebar entity. */
|
|
55975
55999
|
.nav-badge {
|
|
55976
56000
|
display: inline-block; min-width: 16px; text-align: center;
|
|
@@ -57449,6 +57473,12 @@ var appJs = `
|
|
|
57449
57473
|
// drag handle once the app has booted.
|
|
57450
57474
|
var savedRail = parseInt(window.localStorage.getItem(RAIL_KEY) || '', 10);
|
|
57451
57475
|
if (!isNaN(savedRail)) applyRailWidth(savedRail);
|
|
57476
|
+
// The version chip + manual-upgrade link live in the static shell (present
|
|
57477
|
+
// from first paint, in both the normal and virgin-state boots), so wire the
|
|
57478
|
+
// click handler and run the first availability check here \u2014 independent of
|
|
57479
|
+
// the async workspace bootstrap. checkServerVersion() refreshes it later.
|
|
57480
|
+
wireUpdateLink();
|
|
57481
|
+
checkUpdateAvailable();
|
|
57452
57482
|
// Failsafe: never leave the overlay up forever if a fetch hangs without
|
|
57453
57483
|
// rejecting, or a future early-return (e.g. the virgin-state screen)
|
|
57454
57484
|
// bypasses the .then() tail. Idempotent, so a later real hide is a no-op.
|
|
@@ -57933,6 +57963,26 @@ var appJs = `
|
|
|
57933
57963
|
showUpdatePill(label || 'Updated \u2014 reloading\u2026');
|
|
57934
57964
|
setTimeout(function () { location.reload(); }, 600);
|
|
57935
57965
|
}
|
|
57966
|
+
// Manual upgrade fallback: show an "Update available \u2014 Upgrade" link next to
|
|
57967
|
+
// the version chip only when the server reports a newer, installable version.
|
|
57968
|
+
// The auto-updater installs in the background on its own cadence; this lets
|
|
57969
|
+
// the user force it now. Best-effort; the link stays hidden on any failure.
|
|
57970
|
+
function checkUpdateAvailable() {
|
|
57971
|
+
var el = document.getElementById('app-update-link');
|
|
57972
|
+
if (!el) return;
|
|
57973
|
+
fetch('/api/update/status')
|
|
57974
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
57975
|
+
.then(function (s) {
|
|
57976
|
+
if (s && s.latest && s.current && s.latest !== s.current && s.installable) {
|
|
57977
|
+
el.textContent = 'Update available \u2014 Upgrade';
|
|
57978
|
+
el.title = 'Install v' + s.latest + ' and restart';
|
|
57979
|
+
el.hidden = false;
|
|
57980
|
+
} else {
|
|
57981
|
+
el.hidden = true;
|
|
57982
|
+
}
|
|
57983
|
+
})
|
|
57984
|
+
.catch(function () { /* best-effort \u2014 keep the link hidden */ });
|
|
57985
|
+
}
|
|
57936
57986
|
// On every (re)connect, ask the server its version. A change vs BOOT_VERSION
|
|
57937
57987
|
// means a relaunch onto new code \u2192 reload. Best-effort; never throws.
|
|
57938
57988
|
function checkServerVersion() {
|
|
@@ -57946,6 +57996,31 @@ var appJs = `
|
|
|
57946
57996
|
else hideUpdatePill();
|
|
57947
57997
|
})
|
|
57948
57998
|
.catch(function () { /* offline / mid-restart \u2014 the next reconnect retries */ });
|
|
57999
|
+
// Refresh the manual-upgrade link alongside the reconnect version check.
|
|
58000
|
+
checkUpdateAvailable();
|
|
58001
|
+
}
|
|
58002
|
+
// Wire the manual-upgrade link's click: kick off the install (the server
|
|
58003
|
+
// installs the latest and restarts onto it) and surface the progress. On
|
|
58004
|
+
// success we do nothing else \u2014 the update-applied event + the reconnect
|
|
58005
|
+
// version check land the page on the new version (no manual reload). A
|
|
58006
|
+
// false ok means the install can't run (unsupervised) \u2014 toast why.
|
|
58007
|
+
function wireUpdateLink() {
|
|
58008
|
+
var el = document.getElementById('app-update-link');
|
|
58009
|
+
if (!el) return;
|
|
58010
|
+
el.addEventListener('click', function (e) {
|
|
58011
|
+
e.preventDefault();
|
|
58012
|
+
el.hidden = true;
|
|
58013
|
+
showUpdatePill('Updating\u2026');
|
|
58014
|
+
fetch('/api/update/apply', { method: 'POST' })
|
|
58015
|
+
.then(function (r) { return r.json(); })
|
|
58016
|
+
.then(function (d) {
|
|
58017
|
+
if (d && d.ok === false) {
|
|
58018
|
+
hideUpdatePill();
|
|
58019
|
+
showToast(d.error || 'Update unavailable', {});
|
|
58020
|
+
}
|
|
58021
|
+
})
|
|
58022
|
+
.catch(function () { /* server may already be restarting */ });
|
|
58023
|
+
});
|
|
57949
58024
|
}
|
|
57950
58025
|
function dispatchStreamMessage(type, data) {
|
|
57951
58026
|
if (type === 'realtime-state') {
|
|
@@ -64763,6 +64838,7 @@ var guiAppHtml = `<!doctype html>
|
|
|
64763
64838
|
<span class="offline-pill" id="offline-pill" title="Edits queued offline \u2014 will sync when the cloud reconnects" hidden></span>
|
|
64764
64839
|
<span class="app-update" id="app-update" title="A new version is being applied" hidden></span>
|
|
64765
64840
|
<span class="app-version" id="app-version" title="Lattice version"><!--LATTICE_VERSION--></span>
|
|
64841
|
+
<a id="app-update-link" href="#" hidden>Update available \u2014 Upgrade</a>
|
|
64766
64842
|
<button id="settings-gear" title="Settings" aria-label="Open settings">
|
|
64767
64843
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
64768
64844
|
<circle cx="12" cy="12" r="3"/>
|
|
@@ -66278,11 +66354,11 @@ var import_yaml6 = require("yaml");
|
|
|
66278
66354
|
init_lattice();
|
|
66279
66355
|
init_user_config();
|
|
66280
66356
|
init_cloud_connect();
|
|
66281
|
-
var
|
|
66357
|
+
var import_node_crypto20 = require("crypto");
|
|
66282
66358
|
init_members();
|
|
66283
66359
|
|
|
66284
66360
|
// src/cloud/invite.ts
|
|
66285
|
-
var
|
|
66361
|
+
var import_node_crypto19 = require("crypto");
|
|
66286
66362
|
var VERSION = 1;
|
|
66287
66363
|
var SALT_LEN = 16;
|
|
66288
66364
|
var SECRET_LEN = 32;
|
|
@@ -66300,15 +66376,15 @@ function poolerAwareUser(host, role, ownerUser) {
|
|
|
66300
66376
|
return ref ? `${role}.${ref}` : role;
|
|
66301
66377
|
}
|
|
66302
66378
|
function deriveKey2(tokenSecret, email, salt) {
|
|
66303
|
-
const emailSalt = Buffer.from((0,
|
|
66304
|
-
return Buffer.from((0,
|
|
66379
|
+
const emailSalt = Buffer.from((0, import_node_crypto19.scryptSync)(normalizeEmail(email), salt, KEY_LEN));
|
|
66380
|
+
return Buffer.from((0, import_node_crypto19.hkdfSync)("sha256", tokenSecret, emailSalt, HKDF_INFO, KEY_LEN));
|
|
66305
66381
|
}
|
|
66306
66382
|
function mintInviteToken(input) {
|
|
66307
66383
|
const email = normalizeEmail(input.email);
|
|
66308
66384
|
if (!email) throw new Error("lattice: an invite must be bound to an email");
|
|
66309
|
-
const salt = (0,
|
|
66310
|
-
const tokenSecret = (0,
|
|
66311
|
-
const nonce = (0,
|
|
66385
|
+
const salt = (0, import_node_crypto19.randomBytes)(SALT_LEN);
|
|
66386
|
+
const tokenSecret = (0, import_node_crypto19.randomBytes)(SECRET_LEN);
|
|
66387
|
+
const nonce = (0, import_node_crypto19.randomBytes)(NONCE_LEN);
|
|
66312
66388
|
const key = deriveKey2(tokenSecret, email, salt);
|
|
66313
66389
|
const payload = {
|
|
66314
66390
|
v: 1,
|
|
@@ -66322,7 +66398,7 @@ function mintInviteToken(input) {
|
|
|
66322
66398
|
expires_at: input.expiresAt.toISOString(),
|
|
66323
66399
|
...input.workspaceName?.trim() ? { workspace_name: input.workspaceName.trim() } : {}
|
|
66324
66400
|
};
|
|
66325
|
-
const cipher = (0,
|
|
66401
|
+
const cipher = (0, import_node_crypto19.createCipheriv)("aes-256-gcm", key, nonce);
|
|
66326
66402
|
cipher.setAAD(Buffer.from(email, "utf8"));
|
|
66327
66403
|
const ct = Buffer.concat([cipher.update(JSON.stringify(payload), "utf8"), cipher.final()]);
|
|
66328
66404
|
const tag = cipher.getAuthTag();
|
|
@@ -66345,7 +66421,7 @@ function redeemInviteToken(email, token) {
|
|
|
66345
66421
|
const tag = raw.subarray(raw.length - TAG_LEN2);
|
|
66346
66422
|
const ct = raw.subarray(off, raw.length - TAG_LEN2);
|
|
66347
66423
|
const key = deriveKey2(tokenSecret, normEmail, salt);
|
|
66348
|
-
const decipher = (0,
|
|
66424
|
+
const decipher = (0, import_node_crypto19.createDecipheriv)("aes-256-gcm", key, nonce);
|
|
66349
66425
|
decipher.setAAD(Buffer.from(normEmail, "utf8"));
|
|
66350
66426
|
decipher.setAuthTag(tag);
|
|
66351
66427
|
let plaintext;
|
|
@@ -66373,7 +66449,7 @@ function redeemInviteToken(email, token) {
|
|
|
66373
66449
|
init_markdown();
|
|
66374
66450
|
init_rls();
|
|
66375
66451
|
init_adapter();
|
|
66376
|
-
var
|
|
66452
|
+
var import_node_crypto21 = require("crypto");
|
|
66377
66453
|
init_parser();
|
|
66378
66454
|
init_lattice_root();
|
|
66379
66455
|
init_workspace();
|
|
@@ -66458,7 +66534,7 @@ function parseAndValidateLogo(dataUri) {
|
|
|
66458
66534
|
error: `logo must be square (got ${String(dims.width)}\xD7${String(dims.height)})`
|
|
66459
66535
|
};
|
|
66460
66536
|
}
|
|
66461
|
-
return { ok: true, mime, bytes, etag: (0,
|
|
66537
|
+
return { ok: true, mime, bytes, etag: (0, import_node_crypto20.createHash)("sha256").update(bytes).digest("hex") };
|
|
66462
66538
|
}
|
|
66463
66539
|
function updateActiveWorkspaceToCloud(configPath, displayName, key) {
|
|
66464
66540
|
const root6 = findLatticeRoot((0, import_node_path36.dirname)(configPath));
|
|
@@ -66968,7 +67044,7 @@ async function dispatchDbConfigRoute(req, res, ctx) {
|
|
|
66968
67044
|
`INSERT INTO "__lattice_member_invites" ("id","role","email_hash","email","expires_at")
|
|
66969
67045
|
VALUES (?, ?, ?, ?, ?)`,
|
|
66970
67046
|
[
|
|
66971
|
-
(0,
|
|
67047
|
+
(0, import_node_crypto21.randomUUID)(),
|
|
66972
67048
|
role,
|
|
66973
67049
|
emailHash,
|
|
66974
67050
|
// Plaintext email stored ONLY in this owner-only table so the owner's
|
|
@@ -67794,7 +67870,7 @@ var import_node_os9 = require("os");
|
|
|
67794
67870
|
var import_node_path37 = require("path");
|
|
67795
67871
|
init_mutations();
|
|
67796
67872
|
init_extract();
|
|
67797
|
-
var
|
|
67873
|
+
var import_node_crypto22 = require("crypto");
|
|
67798
67874
|
init_assistant_routes();
|
|
67799
67875
|
init_enrich();
|
|
67800
67876
|
init_ingest_url();
|
|
@@ -68035,7 +68111,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
68035
68111
|
let s3Status = null;
|
|
68036
68112
|
const s3cfg = resolveActiveS3Config(ctx.configPath);
|
|
68037
68113
|
if (s3cfg) {
|
|
68038
|
-
const sha256 = blob?.sha256 ?? (0,
|
|
68114
|
+
const sha256 = blob?.sha256 ?? (0, import_node_crypto22.createHash)("sha256").update(buf).digest("hex");
|
|
68039
68115
|
const key = s3Key(s3cfg.prefix, sha256);
|
|
68040
68116
|
try {
|
|
68041
68117
|
const store = await createS3Store(s3cfg);
|
|
@@ -68067,7 +68143,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
68067
68143
|
realPath = rawFilePath;
|
|
68068
68144
|
}
|
|
68069
68145
|
}
|
|
68070
|
-
const fileSha = blob?.sha256 ?? s3Ref?.sha256 ?? (0,
|
|
68146
|
+
const fileSha = blob?.sha256 ?? s3Ref?.sha256 ?? (0, import_node_crypto22.createHash)("sha256").update(buf).digest("hex");
|
|
68071
68147
|
const uploadRow = {
|
|
68072
68148
|
id: fileId,
|
|
68073
68149
|
...fileIdentity(name2, fileId),
|
|
@@ -69305,6 +69381,28 @@ async function startGuiServer(options) {
|
|
|
69305
69381
|
setActive(next, created.id);
|
|
69306
69382
|
return created.id;
|
|
69307
69383
|
};
|
|
69384
|
+
const cleanupWorkspaceFiles = (root6, ws) => {
|
|
69385
|
+
if (!ws.configPath && ws.kind === "local") {
|
|
69386
|
+
(0, import_node_fs35.rmSync)(workspaceDir(root6, ws.dir), { recursive: true, force: true });
|
|
69387
|
+
} else if (ws.kind === "cloud") {
|
|
69388
|
+
if (ws.configPath && (0, import_node_fs35.existsSync)(ws.configPath)) {
|
|
69389
|
+
(0, import_node_fs35.rmSync)(ws.configPath, { force: true });
|
|
69390
|
+
}
|
|
69391
|
+
const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
|
|
69392
|
+
const label = labelMatch?.[1];
|
|
69393
|
+
if (label) {
|
|
69394
|
+
const stillUsed = listWorkspaces(root6).some(
|
|
69395
|
+
(w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
|
|
69396
|
+
);
|
|
69397
|
+
if (!stillUsed) {
|
|
69398
|
+
try {
|
|
69399
|
+
deleteDbCredential(label);
|
|
69400
|
+
} catch {
|
|
69401
|
+
}
|
|
69402
|
+
}
|
|
69403
|
+
}
|
|
69404
|
+
}
|
|
69405
|
+
};
|
|
69308
69406
|
const handleVirginRoute = async (req, res, pathname, method) => {
|
|
69309
69407
|
if (method === "GET" && pathname === "/") {
|
|
69310
69408
|
sendText(
|
|
@@ -69356,6 +69454,35 @@ async function startGuiServer(options) {
|
|
|
69356
69454
|
}
|
|
69357
69455
|
return true;
|
|
69358
69456
|
}
|
|
69457
|
+
if (method === "POST" && pathname === "/api/workspaces/delete") {
|
|
69458
|
+
if (!latticeRoot) {
|
|
69459
|
+
sendJson(res, { error: "No .lattice root \u2014 workspaces unavailable" }, 400);
|
|
69460
|
+
return true;
|
|
69461
|
+
}
|
|
69462
|
+
const body = await readJson(req);
|
|
69463
|
+
if (typeof body.id !== "string") {
|
|
69464
|
+
sendJson(res, { error: "id must be a string" }, 400);
|
|
69465
|
+
return true;
|
|
69466
|
+
}
|
|
69467
|
+
const ws = getWorkspace(latticeRoot, body.id);
|
|
69468
|
+
if (!ws) {
|
|
69469
|
+
sendJson(res, { error: `No workspace with id ${body.id}` }, 400);
|
|
69470
|
+
return true;
|
|
69471
|
+
}
|
|
69472
|
+
removeWorkspace(latticeRoot, ws.id);
|
|
69473
|
+
try {
|
|
69474
|
+
cleanupWorkspaceFiles(latticeRoot, ws);
|
|
69475
|
+
} catch (e6) {
|
|
69476
|
+
sendJson(
|
|
69477
|
+
res,
|
|
69478
|
+
{ error: `Workspace unregistered but file cleanup failed: ${e6.message}` },
|
|
69479
|
+
500
|
|
69480
|
+
);
|
|
69481
|
+
return true;
|
|
69482
|
+
}
|
|
69483
|
+
sendJson(res, { ok: true, switchedTo: null });
|
|
69484
|
+
return true;
|
|
69485
|
+
}
|
|
69359
69486
|
if (method === "POST" && pathname === "/api/cloud/redeem-invite") {
|
|
69360
69487
|
await redeemInvite(createCloudWorkspace, req, res);
|
|
69361
69488
|
return true;
|
|
@@ -69390,6 +69517,18 @@ async function startGuiServer(options) {
|
|
|
69390
69517
|
);
|
|
69391
69518
|
return;
|
|
69392
69519
|
}
|
|
69520
|
+
if (method === "POST" && pathname === "/api/update/apply") {
|
|
69521
|
+
if (updateService) {
|
|
69522
|
+
void updateService.checkNow(true);
|
|
69523
|
+
sendJson(res, { ok: true, status: updateService.status() });
|
|
69524
|
+
} else {
|
|
69525
|
+
sendJson(res, {
|
|
69526
|
+
ok: false,
|
|
69527
|
+
error: "Automatic update is not available for this install. Reinstall from https://latticesql.com to get the latest version."
|
|
69528
|
+
});
|
|
69529
|
+
}
|
|
69530
|
+
return;
|
|
69531
|
+
}
|
|
69393
69532
|
if (!activeRef) {
|
|
69394
69533
|
if (await handleVirginRoute(req, res, pathname, method)) return;
|
|
69395
69534
|
sendJson(res, { error: "No active workspace" }, 409);
|
|
@@ -70483,26 +70622,7 @@ async function startGuiServer(options) {
|
|
|
70483
70622
|
}
|
|
70484
70623
|
removeWorkspace(latticeRoot, ws.id);
|
|
70485
70624
|
try {
|
|
70486
|
-
|
|
70487
|
-
(0, import_node_fs35.rmSync)(workspaceDir(latticeRoot, ws.dir), { recursive: true, force: true });
|
|
70488
|
-
} else if (ws.kind === "cloud") {
|
|
70489
|
-
if (ws.configPath && (0, import_node_fs35.existsSync)(ws.configPath)) {
|
|
70490
|
-
(0, import_node_fs35.rmSync)(ws.configPath, { force: true });
|
|
70491
|
-
}
|
|
70492
|
-
const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
|
|
70493
|
-
const label = labelMatch?.[1];
|
|
70494
|
-
if (label) {
|
|
70495
|
-
const stillUsed = listWorkspaces(latticeRoot).some(
|
|
70496
|
-
(w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
|
|
70497
|
-
);
|
|
70498
|
-
if (!stillUsed) {
|
|
70499
|
-
try {
|
|
70500
|
-
deleteDbCredential(label);
|
|
70501
|
-
} catch {
|
|
70502
|
-
}
|
|
70503
|
-
}
|
|
70504
|
-
}
|
|
70505
|
-
}
|
|
70625
|
+
cleanupWorkspaceFiles(latticeRoot, ws);
|
|
70506
70626
|
} catch (e6) {
|
|
70507
70627
|
sendJson(
|
|
70508
70628
|
res,
|
|
@@ -70968,7 +71088,9 @@ ${e6.stack ?? ""}`
|
|
|
70968
71088
|
}
|
|
70969
71089
|
}
|
|
70970
71090
|
};
|
|
70971
|
-
if (options.
|
|
71091
|
+
if (options.updateServiceFactory) {
|
|
71092
|
+
updateService = options.updateServiceFactory(broadcast);
|
|
71093
|
+
} else if (options.selfUpdate && guiVersion) {
|
|
70972
71094
|
updateService = createUpdateService({ currentVersion: guiVersion, emit: broadcast });
|
|
70973
71095
|
}
|
|
70974
71096
|
const handleEventStream = (ws) => {
|
|
@@ -71110,7 +71232,7 @@ ${e6.stack ?? ""}`
|
|
|
71110
71232
|
// src/cloud/file-source-key-store.ts
|
|
71111
71233
|
var import_node_fs36 = require("fs");
|
|
71112
71234
|
var import_node_path39 = require("path");
|
|
71113
|
-
var
|
|
71235
|
+
var import_node_crypto23 = require("crypto");
|
|
71114
71236
|
var ENC_HEADER = "LATTICE-KMS-v1\n";
|
|
71115
71237
|
var SCRYPT_N = 1 << 15;
|
|
71116
71238
|
var SCRYPT_R = 8;
|
|
@@ -71137,7 +71259,7 @@ var FileSourceKeyStore = class {
|
|
|
71137
71259
|
getOrCreate(sourceId) {
|
|
71138
71260
|
let key = this.cache.get(sourceId);
|
|
71139
71261
|
if (!key) {
|
|
71140
|
-
key = (0,
|
|
71262
|
+
key = (0, import_node_crypto23.randomBytes)(KEY_LEN2);
|
|
71141
71263
|
this.cache.set(sourceId, key);
|
|
71142
71264
|
this.persist();
|
|
71143
71265
|
}
|
|
@@ -71181,7 +71303,7 @@ var FileSourceKeyStore = class {
|
|
|
71181
71303
|
const obj2 = {};
|
|
71182
71304
|
for (const [k6, v2] of this.cache) obj2[k6] = v2.toString("base64");
|
|
71183
71305
|
const encoded = this.encodeFile(obj2);
|
|
71184
|
-
const tmpPath = `${this.path}.tmp-${process.pid.toString()}-${(0,
|
|
71306
|
+
const tmpPath = `${this.path}.tmp-${process.pid.toString()}-${(0, import_node_crypto23.randomBytes)(4).toString("hex")}`;
|
|
71185
71307
|
(0, import_node_fs36.writeFileSync)(tmpPath, encoded, { mode: 384 });
|
|
71186
71308
|
try {
|
|
71187
71309
|
(0, import_node_fs36.chmodSync)(tmpPath, 384);
|
|
@@ -71224,14 +71346,14 @@ var FileSourceKeyStore = class {
|
|
|
71224
71346
|
if (passphrase === void 0) {
|
|
71225
71347
|
throw new Error("lattice: key file is encrypted but no passphrase was configured");
|
|
71226
71348
|
}
|
|
71227
|
-
const derived = (0,
|
|
71349
|
+
const derived = (0, import_node_crypto23.scryptSync)(passphrase, salt, KEY_LEN2, {
|
|
71228
71350
|
N: SCRYPT_N,
|
|
71229
71351
|
r: SCRYPT_R,
|
|
71230
71352
|
p: SCRYPT_P,
|
|
71231
71353
|
maxmem: 64 * 1024 * 1024
|
|
71232
71354
|
// raise Node's default 32MB cap so N=2^15 fits
|
|
71233
71355
|
});
|
|
71234
|
-
const decipher = (0,
|
|
71356
|
+
const decipher = (0, import_node_crypto23.createDecipheriv)("aes-256-gcm", derived, iv);
|
|
71235
71357
|
decipher.setAuthTag(tag);
|
|
71236
71358
|
let plaintext;
|
|
71237
71359
|
try {
|
|
@@ -71246,15 +71368,15 @@ var FileSourceKeyStore = class {
|
|
|
71246
71368
|
if (!this.passphrase) {
|
|
71247
71369
|
return Buffer.from(json, "utf8");
|
|
71248
71370
|
}
|
|
71249
|
-
const salt = (0,
|
|
71250
|
-
const iv = (0,
|
|
71251
|
-
const derived = (0,
|
|
71371
|
+
const salt = (0, import_node_crypto23.randomBytes)(SALT_LEN2);
|
|
71372
|
+
const iv = (0, import_node_crypto23.randomBytes)(IV_LEN2);
|
|
71373
|
+
const derived = (0, import_node_crypto23.scryptSync)(this.passphrase, salt, KEY_LEN2, {
|
|
71252
71374
|
N: SCRYPT_N,
|
|
71253
71375
|
r: SCRYPT_R,
|
|
71254
71376
|
p: SCRYPT_P,
|
|
71255
71377
|
maxmem: 64 * 1024 * 1024
|
|
71256
71378
|
});
|
|
71257
|
-
const cipher = (0,
|
|
71379
|
+
const cipher = (0, import_node_crypto23.createCipheriv)("aes-256-gcm", derived, iv);
|
|
71258
71380
|
const ct = Buffer.concat([cipher.update(json, "utf8"), cipher.final()]);
|
|
71259
71381
|
const tag = cipher.getAuthTag();
|
|
71260
71382
|
const body = `${salt.toString("hex")}:${iv.toString("hex")}:${Buffer.concat([ct, tag]).toString("hex")}`;
|
package/dist/index.d.cts
CHANGED
|
@@ -4923,6 +4923,37 @@ interface PdfOptions {
|
|
|
4923
4923
|
*/
|
|
4924
4924
|
declare function describePdf(auth: ClaudeAuth, path: string, opts?: PdfOptions): Promise<string>;
|
|
4925
4925
|
|
|
4926
|
+
/** How the running copy of the package was installed. */
|
|
4927
|
+
type InstallKind = 'global' | 'local' | 'npx' | 'linked-dev' | 'unknown';
|
|
4928
|
+
interface InstallContext {
|
|
4929
|
+
kind: InstallKind;
|
|
4930
|
+
/** True only when an `npm install` may safely upgrade this copy in place. */
|
|
4931
|
+
installable: boolean;
|
|
4932
|
+
/** Directory to run the install from (the consumer project root for `local`). */
|
|
4933
|
+
cwd: string;
|
|
4934
|
+
/** Resolved package root of the running copy, if found. */
|
|
4935
|
+
packageRoot: string | null;
|
|
4936
|
+
/** Human-readable explanation (logged / surfaced in `/api/update/status`). */
|
|
4937
|
+
reason: string;
|
|
4938
|
+
}
|
|
4939
|
+
|
|
4940
|
+
interface UpdateStatus {
|
|
4941
|
+
current: string;
|
|
4942
|
+
latest: string | null;
|
|
4943
|
+
kind: InstallContext['kind'];
|
|
4944
|
+
installable: boolean;
|
|
4945
|
+
checking: boolean;
|
|
4946
|
+
installing: boolean;
|
|
4947
|
+
lastError: string | null;
|
|
4948
|
+
}
|
|
4949
|
+
interface UpdateService {
|
|
4950
|
+
start(): void;
|
|
4951
|
+
stop(): void;
|
|
4952
|
+
status(): UpdateStatus;
|
|
4953
|
+
/** Run a check now (and install if applicable). Returns the resulting status. */
|
|
4954
|
+
checkNow(force?: boolean): Promise<UpdateStatus>;
|
|
4955
|
+
}
|
|
4956
|
+
|
|
4926
4957
|
interface StartGuiServerOptions {
|
|
4927
4958
|
/**
|
|
4928
4959
|
* Active workspace config to open. NULL/empty ⇒ boot into the zero-workspace
|
|
@@ -4979,6 +5010,13 @@ interface StartGuiServerOptions {
|
|
|
4979
5010
|
* `GET /api/version` + `GET /api/update/status` are served regardless.
|
|
4980
5011
|
*/
|
|
4981
5012
|
selfUpdate?: boolean;
|
|
5013
|
+
/**
|
|
5014
|
+
* Test seam: supply the update service instead of building one from the real
|
|
5015
|
+
* npm-backed install context. Lets tests exercise the update routes against a
|
|
5016
|
+
* deterministic fake (no real registry check, no real npm install). When set,
|
|
5017
|
+
* it overrides `selfUpdate`'s default factory.
|
|
5018
|
+
*/
|
|
5019
|
+
updateServiceFactory?: (emit: (type: string, data: unknown) => void) => UpdateService;
|
|
4982
5020
|
}
|
|
4983
5021
|
interface GuiServerHandle {
|
|
4984
5022
|
server: Server;
|
package/dist/index.d.ts
CHANGED
|
@@ -4923,6 +4923,37 @@ interface PdfOptions {
|
|
|
4923
4923
|
*/
|
|
4924
4924
|
declare function describePdf(auth: ClaudeAuth, path: string, opts?: PdfOptions): Promise<string>;
|
|
4925
4925
|
|
|
4926
|
+
/** How the running copy of the package was installed. */
|
|
4927
|
+
type InstallKind = 'global' | 'local' | 'npx' | 'linked-dev' | 'unknown';
|
|
4928
|
+
interface InstallContext {
|
|
4929
|
+
kind: InstallKind;
|
|
4930
|
+
/** True only when an `npm install` may safely upgrade this copy in place. */
|
|
4931
|
+
installable: boolean;
|
|
4932
|
+
/** Directory to run the install from (the consumer project root for `local`). */
|
|
4933
|
+
cwd: string;
|
|
4934
|
+
/** Resolved package root of the running copy, if found. */
|
|
4935
|
+
packageRoot: string | null;
|
|
4936
|
+
/** Human-readable explanation (logged / surfaced in `/api/update/status`). */
|
|
4937
|
+
reason: string;
|
|
4938
|
+
}
|
|
4939
|
+
|
|
4940
|
+
interface UpdateStatus {
|
|
4941
|
+
current: string;
|
|
4942
|
+
latest: string | null;
|
|
4943
|
+
kind: InstallContext['kind'];
|
|
4944
|
+
installable: boolean;
|
|
4945
|
+
checking: boolean;
|
|
4946
|
+
installing: boolean;
|
|
4947
|
+
lastError: string | null;
|
|
4948
|
+
}
|
|
4949
|
+
interface UpdateService {
|
|
4950
|
+
start(): void;
|
|
4951
|
+
stop(): void;
|
|
4952
|
+
status(): UpdateStatus;
|
|
4953
|
+
/** Run a check now (and install if applicable). Returns the resulting status. */
|
|
4954
|
+
checkNow(force?: boolean): Promise<UpdateStatus>;
|
|
4955
|
+
}
|
|
4956
|
+
|
|
4926
4957
|
interface StartGuiServerOptions {
|
|
4927
4958
|
/**
|
|
4928
4959
|
* Active workspace config to open. NULL/empty ⇒ boot into the zero-workspace
|
|
@@ -4979,6 +5010,13 @@ interface StartGuiServerOptions {
|
|
|
4979
5010
|
* `GET /api/version` + `GET /api/update/status` are served regardless.
|
|
4980
5011
|
*/
|
|
4981
5012
|
selfUpdate?: boolean;
|
|
5013
|
+
/**
|
|
5014
|
+
* Test seam: supply the update service instead of building one from the real
|
|
5015
|
+
* npm-backed install context. Lets tests exercise the update routes against a
|
|
5016
|
+
* deterministic fake (no real registry check, no real npm install). When set,
|
|
5017
|
+
* it overrides `selfUpdate`'s default factory.
|
|
5018
|
+
*/
|
|
5019
|
+
updateServiceFactory?: (emit: (type: string, data: unknown) => void) => UpdateService;
|
|
4982
5020
|
}
|
|
4983
5021
|
interface GuiServerHandle {
|
|
4984
5022
|
server: Server;
|
package/dist/index.js
CHANGED
|
@@ -4531,14 +4531,6 @@ function resolveDbPath(raw, configDir2) {
|
|
|
4531
4531
|
}
|
|
4532
4532
|
return resolve2(configDir2, raw);
|
|
4533
4533
|
}
|
|
4534
|
-
function warnDeprecatedRef(entity, field, target) {
|
|
4535
|
-
const key = `${entity}.${field}`;
|
|
4536
|
-
if (warnedDeprecatedRefs.has(key)) return;
|
|
4537
|
-
warnedDeprecatedRefs.add(key);
|
|
4538
|
-
console.warn(
|
|
4539
|
-
`Lattice: one-to-many \`ref:\` on "${entity}.${field}" \u2192 "${target}" is deprecated in favor of many-to-many junction tables and will be removed in 2.0.`
|
|
4540
|
-
);
|
|
4541
|
-
}
|
|
4542
4534
|
function entityToTableDef(entityName, entity) {
|
|
4543
4535
|
const rawFields = entity.fields;
|
|
4544
4536
|
if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
|
|
@@ -4565,7 +4557,6 @@ function entityToTableDef(entityName, entity) {
|
|
|
4565
4557
|
table: field.ref,
|
|
4566
4558
|
foreignKey: fieldName
|
|
4567
4559
|
};
|
|
4568
|
-
warnDeprecatedRef(entityName, fieldName, field.ref);
|
|
4569
4560
|
}
|
|
4570
4561
|
}
|
|
4571
4562
|
const primaryKey = entity.primaryKey ?? pkFromField;
|
|
@@ -4722,12 +4713,10 @@ function parseEntityContexts(entityContexts) {
|
|
|
4722
4713
|
}
|
|
4723
4714
|
return result;
|
|
4724
4715
|
}
|
|
4725
|
-
var warnedDeprecatedRefs;
|
|
4726
4716
|
var init_parser = __esm({
|
|
4727
4717
|
"src/config/parser.ts"() {
|
|
4728
4718
|
"use strict";
|
|
4729
4719
|
init_user_config();
|
|
4730
|
-
warnedDeprecatedRefs = /* @__PURE__ */ new Set();
|
|
4731
4720
|
}
|
|
4732
4721
|
});
|
|
4733
4722
|
|
|
@@ -50608,6 +50597,21 @@ var init_registry = __esm({
|
|
|
50608
50597
|
["table", "values"]
|
|
50609
50598
|
)
|
|
50610
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
|
+
},
|
|
50611
50615
|
{
|
|
50612
50616
|
name: "create_artifact",
|
|
50613
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).",
|
|
@@ -52814,6 +52818,7 @@ var init_ingest_url = __esm({
|
|
|
52814
52818
|
});
|
|
52815
52819
|
|
|
52816
52820
|
// src/gui/ai/dispatch.ts
|
|
52821
|
+
import { randomUUID } from "crypto";
|
|
52817
52822
|
function visibilityDenialReason(opts) {
|
|
52818
52823
|
if (opts.kind === "table") {
|
|
52819
52824
|
return opts.canManageTableDefault ? null : "Only the workspace owner can change a table's default sharing.";
|
|
@@ -53029,6 +53034,21 @@ async function executeFunction(ctx, name, args) {
|
|
|
53029
53034
|
);
|
|
53030
53035
|
return { ok: true, result: { id } };
|
|
53031
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
|
+
}
|
|
53032
53052
|
case "create_artifact": {
|
|
53033
53053
|
const table = requireTable("files", ctx.validTables);
|
|
53034
53054
|
const title = requireString(args.title, "title");
|
|
@@ -53359,6 +53379,7 @@ var init_dispatch = __esm({
|
|
|
53359
53379
|
"get_history",
|
|
53360
53380
|
"create_row",
|
|
53361
53381
|
"create_artifact",
|
|
53382
|
+
"create_secret",
|
|
53362
53383
|
"ingest_url",
|
|
53363
53384
|
"set_definition",
|
|
53364
53385
|
"set_visibility",
|
|
@@ -55797,6 +55818,8 @@ var css = `
|
|
|
55797
55818
|
.app-version:empty { display: none; }
|
|
55798
55819
|
.app-update { flex: 0 0 auto; color: var(--accent, #4a9); font-size: 12px; white-space: nowrap; }
|
|
55799
55820
|
.app-update[hidden] { display: none; }
|
|
55821
|
+
#app-update-link { flex: 0 0 auto; margin-left: 8px; color: var(--accent, #4a9); font-size: 12px; cursor: pointer; white-space: nowrap; }
|
|
55822
|
+
#app-update-link[hidden] { display: none; }
|
|
55800
55823
|
/* Unseen-change count next to a sidebar entity. */
|
|
55801
55824
|
.nav-badge {
|
|
55802
55825
|
display: inline-block; min-width: 16px; text-align: center;
|
|
@@ -57275,6 +57298,12 @@ var appJs = `
|
|
|
57275
57298
|
// drag handle once the app has booted.
|
|
57276
57299
|
var savedRail = parseInt(window.localStorage.getItem(RAIL_KEY) || '', 10);
|
|
57277
57300
|
if (!isNaN(savedRail)) applyRailWidth(savedRail);
|
|
57301
|
+
// The version chip + manual-upgrade link live in the static shell (present
|
|
57302
|
+
// from first paint, in both the normal and virgin-state boots), so wire the
|
|
57303
|
+
// click handler and run the first availability check here \u2014 independent of
|
|
57304
|
+
// the async workspace bootstrap. checkServerVersion() refreshes it later.
|
|
57305
|
+
wireUpdateLink();
|
|
57306
|
+
checkUpdateAvailable();
|
|
57278
57307
|
// Failsafe: never leave the overlay up forever if a fetch hangs without
|
|
57279
57308
|
// rejecting, or a future early-return (e.g. the virgin-state screen)
|
|
57280
57309
|
// bypasses the .then() tail. Idempotent, so a later real hide is a no-op.
|
|
@@ -57759,6 +57788,26 @@ var appJs = `
|
|
|
57759
57788
|
showUpdatePill(label || 'Updated \u2014 reloading\u2026');
|
|
57760
57789
|
setTimeout(function () { location.reload(); }, 600);
|
|
57761
57790
|
}
|
|
57791
|
+
// Manual upgrade fallback: show an "Update available \u2014 Upgrade" link next to
|
|
57792
|
+
// the version chip only when the server reports a newer, installable version.
|
|
57793
|
+
// The auto-updater installs in the background on its own cadence; this lets
|
|
57794
|
+
// the user force it now. Best-effort; the link stays hidden on any failure.
|
|
57795
|
+
function checkUpdateAvailable() {
|
|
57796
|
+
var el = document.getElementById('app-update-link');
|
|
57797
|
+
if (!el) return;
|
|
57798
|
+
fetch('/api/update/status')
|
|
57799
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
57800
|
+
.then(function (s) {
|
|
57801
|
+
if (s && s.latest && s.current && s.latest !== s.current && s.installable) {
|
|
57802
|
+
el.textContent = 'Update available \u2014 Upgrade';
|
|
57803
|
+
el.title = 'Install v' + s.latest + ' and restart';
|
|
57804
|
+
el.hidden = false;
|
|
57805
|
+
} else {
|
|
57806
|
+
el.hidden = true;
|
|
57807
|
+
}
|
|
57808
|
+
})
|
|
57809
|
+
.catch(function () { /* best-effort \u2014 keep the link hidden */ });
|
|
57810
|
+
}
|
|
57762
57811
|
// On every (re)connect, ask the server its version. A change vs BOOT_VERSION
|
|
57763
57812
|
// means a relaunch onto new code \u2192 reload. Best-effort; never throws.
|
|
57764
57813
|
function checkServerVersion() {
|
|
@@ -57772,6 +57821,31 @@ var appJs = `
|
|
|
57772
57821
|
else hideUpdatePill();
|
|
57773
57822
|
})
|
|
57774
57823
|
.catch(function () { /* offline / mid-restart \u2014 the next reconnect retries */ });
|
|
57824
|
+
// Refresh the manual-upgrade link alongside the reconnect version check.
|
|
57825
|
+
checkUpdateAvailable();
|
|
57826
|
+
}
|
|
57827
|
+
// Wire the manual-upgrade link's click: kick off the install (the server
|
|
57828
|
+
// installs the latest and restarts onto it) and surface the progress. On
|
|
57829
|
+
// success we do nothing else \u2014 the update-applied event + the reconnect
|
|
57830
|
+
// version check land the page on the new version (no manual reload). A
|
|
57831
|
+
// false ok means the install can't run (unsupervised) \u2014 toast why.
|
|
57832
|
+
function wireUpdateLink() {
|
|
57833
|
+
var el = document.getElementById('app-update-link');
|
|
57834
|
+
if (!el) return;
|
|
57835
|
+
el.addEventListener('click', function (e) {
|
|
57836
|
+
e.preventDefault();
|
|
57837
|
+
el.hidden = true;
|
|
57838
|
+
showUpdatePill('Updating\u2026');
|
|
57839
|
+
fetch('/api/update/apply', { method: 'POST' })
|
|
57840
|
+
.then(function (r) { return r.json(); })
|
|
57841
|
+
.then(function (d) {
|
|
57842
|
+
if (d && d.ok === false) {
|
|
57843
|
+
hideUpdatePill();
|
|
57844
|
+
showToast(d.error || 'Update unavailable', {});
|
|
57845
|
+
}
|
|
57846
|
+
})
|
|
57847
|
+
.catch(function () { /* server may already be restarting */ });
|
|
57848
|
+
});
|
|
57775
57849
|
}
|
|
57776
57850
|
function dispatchStreamMessage(type, data) {
|
|
57777
57851
|
if (type === 'realtime-state') {
|
|
@@ -64589,6 +64663,7 @@ var guiAppHtml = `<!doctype html>
|
|
|
64589
64663
|
<span class="offline-pill" id="offline-pill" title="Edits queued offline \u2014 will sync when the cloud reconnects" hidden></span>
|
|
64590
64664
|
<span class="app-update" id="app-update" title="A new version is being applied" hidden></span>
|
|
64591
64665
|
<span class="app-version" id="app-version" title="Lattice version"><!--LATTICE_VERSION--></span>
|
|
64666
|
+
<a id="app-update-link" href="#" hidden>Update available \u2014 Upgrade</a>
|
|
64592
64667
|
<button id="settings-gear" title="Settings" aria-label="Open settings">
|
|
64593
64668
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
64594
64669
|
<circle cx="12" cy="12" r="3"/>
|
|
@@ -66198,7 +66273,7 @@ function redeemInviteToken(email, token) {
|
|
|
66198
66273
|
init_markdown();
|
|
66199
66274
|
init_rls();
|
|
66200
66275
|
init_adapter();
|
|
66201
|
-
import { randomUUID } from "crypto";
|
|
66276
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
66202
66277
|
init_parser();
|
|
66203
66278
|
init_lattice_root();
|
|
66204
66279
|
init_workspace();
|
|
@@ -66793,7 +66868,7 @@ async function dispatchDbConfigRoute(req, res, ctx) {
|
|
|
66793
66868
|
`INSERT INTO "__lattice_member_invites" ("id","role","email_hash","email","expires_at")
|
|
66794
66869
|
VALUES (?, ?, ?, ?, ?)`,
|
|
66795
66870
|
[
|
|
66796
|
-
|
|
66871
|
+
randomUUID2(),
|
|
66797
66872
|
role,
|
|
66798
66873
|
emailHash,
|
|
66799
66874
|
// Plaintext email stored ONLY in this owner-only table so the owner's
|
|
@@ -69130,6 +69205,28 @@ async function startGuiServer(options) {
|
|
|
69130
69205
|
setActive(next, created.id);
|
|
69131
69206
|
return created.id;
|
|
69132
69207
|
};
|
|
69208
|
+
const cleanupWorkspaceFiles = (root6, ws) => {
|
|
69209
|
+
if (!ws.configPath && ws.kind === "local") {
|
|
69210
|
+
rmSync(workspaceDir(root6, ws.dir), { recursive: true, force: true });
|
|
69211
|
+
} else if (ws.kind === "cloud") {
|
|
69212
|
+
if (ws.configPath && existsSync24(ws.configPath)) {
|
|
69213
|
+
rmSync(ws.configPath, { force: true });
|
|
69214
|
+
}
|
|
69215
|
+
const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
|
|
69216
|
+
const label = labelMatch?.[1];
|
|
69217
|
+
if (label) {
|
|
69218
|
+
const stillUsed = listWorkspaces(root6).some(
|
|
69219
|
+
(w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
|
|
69220
|
+
);
|
|
69221
|
+
if (!stillUsed) {
|
|
69222
|
+
try {
|
|
69223
|
+
deleteDbCredential(label);
|
|
69224
|
+
} catch {
|
|
69225
|
+
}
|
|
69226
|
+
}
|
|
69227
|
+
}
|
|
69228
|
+
}
|
|
69229
|
+
};
|
|
69133
69230
|
const handleVirginRoute = async (req, res, pathname, method) => {
|
|
69134
69231
|
if (method === "GET" && pathname === "/") {
|
|
69135
69232
|
sendText(
|
|
@@ -69181,6 +69278,35 @@ async function startGuiServer(options) {
|
|
|
69181
69278
|
}
|
|
69182
69279
|
return true;
|
|
69183
69280
|
}
|
|
69281
|
+
if (method === "POST" && pathname === "/api/workspaces/delete") {
|
|
69282
|
+
if (!latticeRoot) {
|
|
69283
|
+
sendJson(res, { error: "No .lattice root \u2014 workspaces unavailable" }, 400);
|
|
69284
|
+
return true;
|
|
69285
|
+
}
|
|
69286
|
+
const body = await readJson(req);
|
|
69287
|
+
if (typeof body.id !== "string") {
|
|
69288
|
+
sendJson(res, { error: "id must be a string" }, 400);
|
|
69289
|
+
return true;
|
|
69290
|
+
}
|
|
69291
|
+
const ws = getWorkspace(latticeRoot, body.id);
|
|
69292
|
+
if (!ws) {
|
|
69293
|
+
sendJson(res, { error: `No workspace with id ${body.id}` }, 400);
|
|
69294
|
+
return true;
|
|
69295
|
+
}
|
|
69296
|
+
removeWorkspace(latticeRoot, ws.id);
|
|
69297
|
+
try {
|
|
69298
|
+
cleanupWorkspaceFiles(latticeRoot, ws);
|
|
69299
|
+
} catch (e6) {
|
|
69300
|
+
sendJson(
|
|
69301
|
+
res,
|
|
69302
|
+
{ error: `Workspace unregistered but file cleanup failed: ${e6.message}` },
|
|
69303
|
+
500
|
|
69304
|
+
);
|
|
69305
|
+
return true;
|
|
69306
|
+
}
|
|
69307
|
+
sendJson(res, { ok: true, switchedTo: null });
|
|
69308
|
+
return true;
|
|
69309
|
+
}
|
|
69184
69310
|
if (method === "POST" && pathname === "/api/cloud/redeem-invite") {
|
|
69185
69311
|
await redeemInvite(createCloudWorkspace, req, res);
|
|
69186
69312
|
return true;
|
|
@@ -69215,6 +69341,18 @@ async function startGuiServer(options) {
|
|
|
69215
69341
|
);
|
|
69216
69342
|
return;
|
|
69217
69343
|
}
|
|
69344
|
+
if (method === "POST" && pathname === "/api/update/apply") {
|
|
69345
|
+
if (updateService) {
|
|
69346
|
+
void updateService.checkNow(true);
|
|
69347
|
+
sendJson(res, { ok: true, status: updateService.status() });
|
|
69348
|
+
} else {
|
|
69349
|
+
sendJson(res, {
|
|
69350
|
+
ok: false,
|
|
69351
|
+
error: "Automatic update is not available for this install. Reinstall from https://latticesql.com to get the latest version."
|
|
69352
|
+
});
|
|
69353
|
+
}
|
|
69354
|
+
return;
|
|
69355
|
+
}
|
|
69218
69356
|
if (!activeRef) {
|
|
69219
69357
|
if (await handleVirginRoute(req, res, pathname, method)) return;
|
|
69220
69358
|
sendJson(res, { error: "No active workspace" }, 409);
|
|
@@ -70308,26 +70446,7 @@ async function startGuiServer(options) {
|
|
|
70308
70446
|
}
|
|
70309
70447
|
removeWorkspace(latticeRoot, ws.id);
|
|
70310
70448
|
try {
|
|
70311
|
-
|
|
70312
|
-
rmSync(workspaceDir(latticeRoot, ws.dir), { recursive: true, force: true });
|
|
70313
|
-
} else if (ws.kind === "cloud") {
|
|
70314
|
-
if (ws.configPath && existsSync24(ws.configPath)) {
|
|
70315
|
-
rmSync(ws.configPath, { force: true });
|
|
70316
|
-
}
|
|
70317
|
-
const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
|
|
70318
|
-
const label = labelMatch?.[1];
|
|
70319
|
-
if (label) {
|
|
70320
|
-
const stillUsed = listWorkspaces(latticeRoot).some(
|
|
70321
|
-
(w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
|
|
70322
|
-
);
|
|
70323
|
-
if (!stillUsed) {
|
|
70324
|
-
try {
|
|
70325
|
-
deleteDbCredential(label);
|
|
70326
|
-
} catch {
|
|
70327
|
-
}
|
|
70328
|
-
}
|
|
70329
|
-
}
|
|
70330
|
-
}
|
|
70449
|
+
cleanupWorkspaceFiles(latticeRoot, ws);
|
|
70331
70450
|
} catch (e6) {
|
|
70332
70451
|
sendJson(
|
|
70333
70452
|
res,
|
|
@@ -70793,7 +70912,9 @@ ${e6.stack ?? ""}`
|
|
|
70793
70912
|
}
|
|
70794
70913
|
}
|
|
70795
70914
|
};
|
|
70796
|
-
if (options.
|
|
70915
|
+
if (options.updateServiceFactory) {
|
|
70916
|
+
updateService = options.updateServiceFactory(broadcast);
|
|
70917
|
+
} else if (options.selfUpdate && guiVersion) {
|
|
70797
70918
|
updateService = createUpdateService({ currentVersion: guiVersion, emit: broadcast });
|
|
70798
70919
|
}
|
|
70799
70920
|
const handleEventStream = (ws) => {
|
package/package.json
CHANGED