sproutkit-code 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +321 -31
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -225,6 +225,10 @@ var require_db = __commonJS({
|
|
|
225
225
|
// redeploys: SESSION_SECRET is regenerated every deploy, so deriving from
|
|
226
226
|
// it would make the client's self-chosen password undecryptable each time.
|
|
227
227
|
"auth_enc_key TEXT",
|
|
228
|
+
// Read-only per-site key so `sproutkit report` can read visitor stats
|
|
229
|
+
// WITHOUT the client's password. Means handing the client full ownership
|
|
230
|
+
// doesn't cost the agency its reporting.
|
|
231
|
+
"report_key TEXT",
|
|
228
232
|
// Reply loop: when we last contacted them, and the latest outcome. Kept on
|
|
229
233
|
// the lead (as well as contact_log) so `followups` is a single cheap query.
|
|
230
234
|
"last_contacted_at TEXT",
|
|
@@ -31319,6 +31323,7 @@ var require_analytics_runtime = __commonJS({
|
|
|
31319
31323
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
31320
31324
|
exports2.TRACK_SNIPPET = exports2.API_STATS_JS = exports2.API_TRACK_JS = void 0;
|
|
31321
31325
|
exports2.API_TRACK_JS = `import { list, put } from "@vercel/blob";
|
|
31326
|
+
import { createHash, createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
31322
31327
|
|
|
31323
31328
|
export const config = { runtime: "nodejs" };
|
|
31324
31329
|
|
|
@@ -31333,6 +31338,54 @@ const EVENTS = ["view", "call", "book", "directions", "email", "menu"];
|
|
|
31333
31338
|
// the file has to stay small enough to read on every request.
|
|
31334
31339
|
const MAX_DAYS = 400;
|
|
31335
31340
|
|
|
31341
|
+
|
|
31342
|
+
// stats.json lives in a PUBLIC Blob at a non-random path, and the store
|
|
31343
|
+
// hostname leaks through every photo URL on the site \u2014 so the file's URL must
|
|
31344
|
+
// be assumed guessable. Visitor counts are the client's commercial data and the
|
|
31345
|
+
// agency's renewal evidence, so the contents are encrypted with AES-256-GCM
|
|
31346
|
+
// under AUTH_ENC_KEY (a server-side env var). A direct public read yields
|
|
31347
|
+
// opaque bytes instead of the numbers.
|
|
31348
|
+
function statsKey() {
|
|
31349
|
+
const material = process.env.AUTH_ENC_KEY || "";
|
|
31350
|
+
if (!material) return null;
|
|
31351
|
+
return createHash("sha256").update("sproutkit-stats:" + material).digest();
|
|
31352
|
+
}
|
|
31353
|
+
|
|
31354
|
+
function sealStats(obj) {
|
|
31355
|
+
const key = statsKey();
|
|
31356
|
+
if (!key) return JSON.stringify({ v: 0, plain: obj });
|
|
31357
|
+
const iv = randomBytes(12);
|
|
31358
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
31359
|
+
const ct = Buffer.concat([cipher.update(JSON.stringify(obj), "utf8"), cipher.final()]);
|
|
31360
|
+
return JSON.stringify({
|
|
31361
|
+
v: 1,
|
|
31362
|
+
iv: iv.toString("base64"),
|
|
31363
|
+
tag: cipher.getAuthTag().toString("base64"),
|
|
31364
|
+
ct: ct.toString("base64"),
|
|
31365
|
+
});
|
|
31366
|
+
}
|
|
31367
|
+
|
|
31368
|
+
function openStats(raw) {
|
|
31369
|
+
if (!raw || typeof raw !== "object") return null;
|
|
31370
|
+
// Unencrypted shapes: v0 (no key available) and pre-encryption files that
|
|
31371
|
+
// stored { days } directly. Both must keep working.
|
|
31372
|
+
if (raw.v === 0 && raw.plain) return raw.plain;
|
|
31373
|
+
if (raw.days && raw.v === undefined) return raw;
|
|
31374
|
+
if (raw.v !== 1) return null;
|
|
31375
|
+
const key = statsKey();
|
|
31376
|
+
if (!key) return null;
|
|
31377
|
+
try {
|
|
31378
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(raw.iv, "base64"));
|
|
31379
|
+
decipher.setAuthTag(Buffer.from(raw.tag, "base64"));
|
|
31380
|
+
return JSON.parse(
|
|
31381
|
+
Buffer.concat([decipher.update(Buffer.from(raw.ct, "base64")), decipher.final()]).toString("utf8")
|
|
31382
|
+
);
|
|
31383
|
+
} catch {
|
|
31384
|
+
// Wrong key or tampered file \u2014 start fresh rather than throwing at a visitor.
|
|
31385
|
+
return null;
|
|
31386
|
+
}
|
|
31387
|
+
}
|
|
31388
|
+
|
|
31336
31389
|
function today() {
|
|
31337
31390
|
return new Date().toISOString().slice(0, 10);
|
|
31338
31391
|
}
|
|
@@ -31344,7 +31397,7 @@ async function readStats() {
|
|
|
31344
31397
|
if (!target) return { days: {} };
|
|
31345
31398
|
const res = await fetch(target.url, { cache: "no-store" });
|
|
31346
31399
|
if (!res.ok) return { days: {} };
|
|
31347
|
-
const j = await res.json();
|
|
31400
|
+
const j = openStats(await res.json());
|
|
31348
31401
|
return j && typeof j === "object" && j.days ? j : { days: {} };
|
|
31349
31402
|
} catch {
|
|
31350
31403
|
return { days: {} };
|
|
@@ -31391,7 +31444,7 @@ export default async function handler(req, res) {
|
|
|
31391
31444
|
}
|
|
31392
31445
|
|
|
31393
31446
|
try {
|
|
31394
|
-
await put(STATS_KEY,
|
|
31447
|
+
await put(STATS_KEY, sealStats(stats), {
|
|
31395
31448
|
access: "public",
|
|
31396
31449
|
addRandomSuffix: false,
|
|
31397
31450
|
allowOverwrite: true,
|
|
@@ -31404,12 +31457,110 @@ export default async function handler(req, res) {
|
|
|
31404
31457
|
}
|
|
31405
31458
|
`;
|
|
31406
31459
|
exports2.API_STATS_JS = `import { list } from "@vercel/blob";
|
|
31407
|
-
import {
|
|
31460
|
+
import {
|
|
31461
|
+
createHmac,
|
|
31462
|
+
timingSafeEqual,
|
|
31463
|
+
createHash,
|
|
31464
|
+
createCipheriv,
|
|
31465
|
+
createDecipheriv,
|
|
31466
|
+
randomBytes,
|
|
31467
|
+
} from "node:crypto";
|
|
31408
31468
|
|
|
31409
31469
|
export const config = { runtime: "nodejs" };
|
|
31410
31470
|
|
|
31411
31471
|
const PFX = process.env.SITE_ID ? "sites/" + process.env.SITE_ID + "/" : "";
|
|
31412
31472
|
const STATS_KEY = PFX + "stats.json";
|
|
31473
|
+
const AUTH_KEY = PFX + "admin-auth.json";
|
|
31474
|
+
|
|
31475
|
+
|
|
31476
|
+
// stats.json lives in a PUBLIC Blob at a non-random path, and the store
|
|
31477
|
+
// hostname leaks through every photo URL on the site \u2014 so the file's URL must
|
|
31478
|
+
// be assumed guessable. Visitor counts are the client's commercial data and the
|
|
31479
|
+
// agency's renewal evidence, so the contents are encrypted with AES-256-GCM
|
|
31480
|
+
// under AUTH_ENC_KEY (a server-side env var). A direct public read yields
|
|
31481
|
+
// opaque bytes instead of the numbers.
|
|
31482
|
+
function statsKey() {
|
|
31483
|
+
const material = process.env.AUTH_ENC_KEY || "";
|
|
31484
|
+
if (!material) return null;
|
|
31485
|
+
return createHash("sha256").update("sproutkit-stats:" + material).digest();
|
|
31486
|
+
}
|
|
31487
|
+
|
|
31488
|
+
function sealStats(obj) {
|
|
31489
|
+
const key = statsKey();
|
|
31490
|
+
if (!key) return JSON.stringify({ v: 0, plain: obj });
|
|
31491
|
+
const iv = randomBytes(12);
|
|
31492
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
31493
|
+
const ct = Buffer.concat([cipher.update(JSON.stringify(obj), "utf8"), cipher.final()]);
|
|
31494
|
+
return JSON.stringify({
|
|
31495
|
+
v: 1,
|
|
31496
|
+
iv: iv.toString("base64"),
|
|
31497
|
+
tag: cipher.getAuthTag().toString("base64"),
|
|
31498
|
+
ct: ct.toString("base64"),
|
|
31499
|
+
});
|
|
31500
|
+
}
|
|
31501
|
+
|
|
31502
|
+
function openStats(raw) {
|
|
31503
|
+
if (!raw || typeof raw !== "object") return null;
|
|
31504
|
+
// Unencrypted shapes: v0 (no key available) and pre-encryption files that
|
|
31505
|
+
// stored { days } directly. Both must keep working.
|
|
31506
|
+
if (raw.v === 0 && raw.plain) return raw.plain;
|
|
31507
|
+
if (raw.days && raw.v === undefined) return raw;
|
|
31508
|
+
if (raw.v !== 1) return null;
|
|
31509
|
+
const key = statsKey();
|
|
31510
|
+
if (!key) return null;
|
|
31511
|
+
try {
|
|
31512
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(raw.iv, "base64"));
|
|
31513
|
+
decipher.setAuthTag(Buffer.from(raw.tag, "base64"));
|
|
31514
|
+
return JSON.parse(
|
|
31515
|
+
Buffer.concat([decipher.update(Buffer.from(raw.ct, "base64")), decipher.final()]).toString("utf8")
|
|
31516
|
+
);
|
|
31517
|
+
} catch {
|
|
31518
|
+
// Wrong key or tampered file \u2014 start fresh rather than throwing at a visitor.
|
|
31519
|
+
return null;
|
|
31520
|
+
}
|
|
31521
|
+
}
|
|
31522
|
+
|
|
31523
|
+
|
|
31524
|
+
// Tokens carry the credential version they were minted under. A password change
|
|
31525
|
+
// bumps it, so cookies issued earlier stop working \u2014 without this, "I changed my
|
|
31526
|
+
// password" wouldn't revoke anyone for up to 8 hours, and the client couldn't
|
|
31527
|
+
// actually take their site back from the agency.
|
|
31528
|
+
//
|
|
31529
|
+
// Fails OPEN when the blob is unreadable: a Blob hiccup must not lock an owner
|
|
31530
|
+
// out of their own site. The password itself is still required to get a token.
|
|
31531
|
+
async function tokenVersionOk(data) {
|
|
31532
|
+
try {
|
|
31533
|
+
const { blobs } = await list({ prefix: AUTH_KEY, limit: 1 });
|
|
31534
|
+
const target = blobs.find((b) => b.pathname === AUTH_KEY);
|
|
31535
|
+
if (!target) return true;
|
|
31536
|
+
const res = await fetch(target.url, { cache: "no-store" });
|
|
31537
|
+
if (!res.ok) return true;
|
|
31538
|
+
const rec = openStatsRecord(await res.json());
|
|
31539
|
+
const current = rec && Number(rec.tokenVersion) ? Number(rec.tokenVersion) : 0;
|
|
31540
|
+
return (Number(data && data.ver) || 0) >= current;
|
|
31541
|
+
} catch {
|
|
31542
|
+
return true;
|
|
31543
|
+
}
|
|
31544
|
+
}
|
|
31545
|
+
|
|
31546
|
+
// admin-auth.json is sealed with a different key label than stats.json.
|
|
31547
|
+
function openStatsRecord(raw) {
|
|
31548
|
+
if (!raw || typeof raw !== "object") return null;
|
|
31549
|
+
if (raw.v === 0 && raw.plain) return raw.plain;
|
|
31550
|
+
if (raw.v !== 1) return null;
|
|
31551
|
+
const material = process.env.AUTH_ENC_KEY || "";
|
|
31552
|
+
if (!material) return null;
|
|
31553
|
+
try {
|
|
31554
|
+
const key = createHash("sha256").update("sproutkit-admin-auth:" + material).digest();
|
|
31555
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(raw.iv, "base64"));
|
|
31556
|
+
decipher.setAuthTag(Buffer.from(raw.tag, "base64"));
|
|
31557
|
+
return JSON.parse(
|
|
31558
|
+
Buffer.concat([decipher.update(Buffer.from(raw.ct, "base64")), decipher.final()]).toString("utf8")
|
|
31559
|
+
);
|
|
31560
|
+
} catch {
|
|
31561
|
+
return null;
|
|
31562
|
+
}
|
|
31563
|
+
}
|
|
31413
31564
|
|
|
31414
31565
|
function verifySession(req) {
|
|
31415
31566
|
const cookie = req.headers.cookie || "";
|
|
@@ -31427,7 +31578,8 @@ function verifySession(req) {
|
|
|
31427
31578
|
const data = JSON.parse(
|
|
31428
31579
|
Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8")
|
|
31429
31580
|
);
|
|
31430
|
-
|
|
31581
|
+
if (!(typeof data.exp === "number" && data.exp > Date.now() / 1000)) return false;
|
|
31582
|
+
return data;
|
|
31431
31583
|
} catch {
|
|
31432
31584
|
return false;
|
|
31433
31585
|
}
|
|
@@ -31450,8 +31602,32 @@ function shiftDays(n) {
|
|
|
31450
31602
|
return d.toISOString().slice(0, 10);
|
|
31451
31603
|
}
|
|
31452
31604
|
|
|
31605
|
+
// The agency needs these numbers for renewal conversations, but must NOT need
|
|
31606
|
+
// the client's password to get them \u2014 otherwise the agency permanently holds the
|
|
31607
|
+
// client's login, and the client changing it silently breaks reporting.
|
|
31608
|
+
//
|
|
31609
|
+
// REPORT_KEY is a separate, per-site, read-only credential. It is accepted ONLY
|
|
31610
|
+
// here: it cannot edit content, read bookings, or change the password. So handing
|
|
31611
|
+
// the client full ownership and keeping agency reporting are no longer in
|
|
31612
|
+
// conflict.
|
|
31613
|
+
function verifyReportKey(req) {
|
|
31614
|
+
const expected = process.env.REPORT_KEY;
|
|
31615
|
+
if (!expected) return false;
|
|
31616
|
+
const header = req.headers["authorization"] || req.headers["x-report-key"] || "";
|
|
31617
|
+
const provided = String(header).replace(/^Bearer\\s+/i, "");
|
|
31618
|
+
if (!provided || provided.length !== expected.length) return false;
|
|
31619
|
+
try {
|
|
31620
|
+
return timingSafeEqual(Buffer.from(provided), Buffer.from(expected));
|
|
31621
|
+
} catch {
|
|
31622
|
+
return false;
|
|
31623
|
+
}
|
|
31624
|
+
}
|
|
31625
|
+
|
|
31453
31626
|
export default async function handler(req, res) {
|
|
31454
|
-
|
|
31627
|
+
const claims = verifySession(req);
|
|
31628
|
+
const viaSession = claims ? await tokenVersionOk(claims) : false;
|
|
31629
|
+
const viaReportKey = viaSession ? false : verifyReportKey(req);
|
|
31630
|
+
if (!viaSession && !viaReportKey) {
|
|
31455
31631
|
res.status(401).json({ error: "unauthorized" });
|
|
31456
31632
|
return;
|
|
31457
31633
|
}
|
|
@@ -31462,7 +31638,7 @@ export default async function handler(req, res) {
|
|
|
31462
31638
|
if (target) {
|
|
31463
31639
|
const r = await fetch(target.url, { cache: "no-store" });
|
|
31464
31640
|
if (r.ok) {
|
|
31465
|
-
const j = await r.json();
|
|
31641
|
+
const j = openStats(await r.json());
|
|
31466
31642
|
if (j && j.days) stats = j;
|
|
31467
31643
|
}
|
|
31468
31644
|
}
|
|
@@ -31473,6 +31649,7 @@ export default async function handler(req, res) {
|
|
|
31473
31649
|
const today = shiftDays(0);
|
|
31474
31650
|
res.setHeader("Cache-Control", "no-store");
|
|
31475
31651
|
res.status(200).json({
|
|
31652
|
+
via: viaSession ? "owner" : "report_key",
|
|
31476
31653
|
last30: windowTotals(stats.days, shiftDays(-29), today),
|
|
31477
31654
|
// The comparison window is what makes the number mean something.
|
|
31478
31655
|
prev30: windowTotals(stats.days, shiftDays(-59), shiftDays(-30)),
|
|
@@ -33339,7 +33516,7 @@ var require_cms_runtime = __commonJS({
|
|
|
33339
33516
|
};
|
|
33340
33517
|
})();
|
|
33341
33518
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
33342
|
-
exports2.ADMIN_APP_JS = exports2.ADMIN_INDEX_HTML = exports2.API_ADMIN_UPLOAD_JS = exports2.API_ADMIN_PASSWORD_JS = exports2.API_ADMIN_WHOAMI_JS = exports2.API_ADMIN_LOGIN_JS = exports2.API_CONTENT_JS = void 0;
|
|
33519
|
+
exports2.ADMIN_APP_JS = exports2.ADMIN_INDEX_HTML = exports2.API_ADMIN_UPLOAD_JS = exports2.API_ADMIN_LOGOUT_JS = exports2.API_ADMIN_PASSWORD_JS = exports2.API_ADMIN_WHOAMI_JS = exports2.API_ADMIN_LOGIN_JS = exports2.API_CONTENT_JS = void 0;
|
|
33343
33520
|
exports2.generateFriendlyPassword = generateFriendlyPassword;
|
|
33344
33521
|
exports2.hashPassword = hashPassword;
|
|
33345
33522
|
exports2.generateSessionSecret = generateSessionSecret;
|
|
@@ -33715,11 +33892,23 @@ export default async function handler(req, res) {
|
|
|
33715
33892
|
delete throttle[who];
|
|
33716
33893
|
await writeJson(THROTTLE_KEY, throttle);
|
|
33717
33894
|
}
|
|
33718
|
-
|
|
33719
|
-
|
|
33895
|
+
// 2h, not 8h. /api/stats checks the credential version and revokes instantly,
|
|
33896
|
+
// but the content/upload/booking endpoints don't (each is a standalone
|
|
33897
|
+
// serverless file and adding the check to all of them means duplicating the
|
|
33898
|
+
// auth-blob crypto five more times). So a cookie issued before a password
|
|
33899
|
+
// change can still WRITE for up to this long. Two hours keeps that window
|
|
33900
|
+
// small; the honest fix is a shared auth module, which needs a bundler for the
|
|
33901
|
+
// generated site.
|
|
33902
|
+
const exp = Math.floor(Date.now() / 1000) + 60 * 60 * 2;
|
|
33903
|
+
// Stamp the credential version into the token. Changing the password bumps
|
|
33904
|
+
// this, which instantly invalidates every cookie issued before the change \u2014
|
|
33905
|
+
// otherwise "I changed my password" wouldn't actually lock anyone out for up
|
|
33906
|
+
// to 8 hours, which makes handover a false promise.
|
|
33907
|
+
const ver = (override && Number(override.tokenVersion)) || 0;
|
|
33908
|
+
const token = sign({ role: "admin", ver, exp }, secret);
|
|
33720
33909
|
res.setHeader(
|
|
33721
33910
|
"Set-Cookie",
|
|
33722
|
-
"sk_admin=" + token + "; Path=/; HttpOnly; SameSite=Lax; Secure; Max-Age=" + 60 * 60 *
|
|
33911
|
+
"sk_admin=" + token + "; Path=/; HttpOnly; SameSite=Lax; Secure; Max-Age=" + 60 * 60 * 2
|
|
33723
33912
|
);
|
|
33724
33913
|
res.status(200).json({ ok: true });
|
|
33725
33914
|
}
|
|
@@ -33861,7 +34050,7 @@ function readBody(req) {
|
|
|
33861
34050
|
});
|
|
33862
34051
|
}
|
|
33863
34052
|
|
|
33864
|
-
async function
|
|
34053
|
+
async function currentRecord() {
|
|
33865
34054
|
try {
|
|
33866
34055
|
const { blobs } = await list({ prefix: AUTH_KEY, limit: 1 });
|
|
33867
34056
|
const target = blobs.find((b) => b.pathname === AUTH_KEY);
|
|
@@ -33869,13 +34058,13 @@ async function currentHash() {
|
|
|
33869
34058
|
const res = await fetch(target.url, { cache: "no-store" });
|
|
33870
34059
|
if (res.ok) {
|
|
33871
34060
|
const j = openAuth(await res.json());
|
|
33872
|
-
if (j && typeof j.passwordHash === "string" && j.passwordHash) return j
|
|
34061
|
+
if (j && typeof j.passwordHash === "string" && j.passwordHash) return j;
|
|
33873
34062
|
}
|
|
33874
34063
|
}
|
|
33875
34064
|
} catch {
|
|
33876
34065
|
// fall through to the env var
|
|
33877
34066
|
}
|
|
33878
|
-
return
|
|
34067
|
+
return null;
|
|
33879
34068
|
}
|
|
33880
34069
|
|
|
33881
34070
|
export default async function handler(req, res) {
|
|
@@ -33916,7 +34105,8 @@ export default async function handler(req, res) {
|
|
|
33916
34105
|
return;
|
|
33917
34106
|
}
|
|
33918
34107
|
|
|
33919
|
-
const
|
|
34108
|
+
const existingRecord = await currentRecord();
|
|
34109
|
+
const existing = existingRecord ? existingRecord.passwordHash : process.env.ADMIN_PASSWORD_HASH || "";
|
|
33920
34110
|
if (!existing) {
|
|
33921
34111
|
res.status(500).json({ error: "not_configured" });
|
|
33922
34112
|
return;
|
|
@@ -33933,7 +34123,13 @@ export default async function handler(req, res) {
|
|
|
33933
34123
|
try {
|
|
33934
34124
|
// clientChanged is read (unencrypted) by nothing sensitive \u2014 it only tells
|
|
33935
34125
|
// the agency "your stored copy is no longer the live password".
|
|
33936
|
-
|
|
34126
|
+
const nextVersion = (Number(existingRecord && existingRecord.tokenVersion) || 0) + 1;
|
|
34127
|
+
await put(AUTH_KEY, sealAuth({
|
|
34128
|
+
passwordHash,
|
|
34129
|
+
updatedAt: new Date().toISOString(),
|
|
34130
|
+
clientChanged: true,
|
|
34131
|
+
tokenVersion: nextVersion,
|
|
34132
|
+
}), {
|
|
33937
34133
|
access: "public",
|
|
33938
34134
|
addRandomSuffix: false,
|
|
33939
34135
|
allowOverwrite: true,
|
|
@@ -33943,6 +34139,20 @@ export default async function handler(req, res) {
|
|
|
33943
34139
|
res.status(500).json({ error: "save_failed", detail: (e && e.message) || "unknown" });
|
|
33944
34140
|
return;
|
|
33945
34141
|
}
|
|
34142
|
+
// Clear the caller's own cookie too \u2014 their old token is now invalid, so
|
|
34143
|
+
// leaving it set would just produce confusing 401s until they reload.
|
|
34144
|
+
res.setHeader("Set-Cookie", "sk_admin=; Path=/; HttpOnly; SameSite=Lax; Secure; Max-Age=0");
|
|
34145
|
+
res.status(200).json({ ok: true, signedOut: true });
|
|
34146
|
+
}
|
|
34147
|
+
`;
|
|
34148
|
+
exports2.API_ADMIN_LOGOUT_JS = `export const config = { runtime: "nodejs" };
|
|
34149
|
+
|
|
34150
|
+
export default function handler(req, res) {
|
|
34151
|
+
if (req.method !== "POST") {
|
|
34152
|
+
res.status(405).json({ error: "method_not_allowed" });
|
|
34153
|
+
return;
|
|
34154
|
+
}
|
|
34155
|
+
res.setHeader("Set-Cookie", "sk_admin=; Path=/; HttpOnly; SameSite=Lax; Secure; Max-Age=0");
|
|
33946
34156
|
res.status(200).json({ ok: true });
|
|
33947
34157
|
}
|
|
33948
34158
|
`;
|
|
@@ -34727,7 +34937,11 @@ export default async function handler(req, res) {
|
|
|
34727
34937
|
}
|
|
34728
34938
|
|
|
34729
34939
|
function TabBar(props) {
|
|
34730
|
-
|
|
34940
|
+
async function signOut() {
|
|
34941
|
+
try { await fetch("/api/admin/logout", { method: "POST", credentials: "same-origin" }); } catch (e) {}
|
|
34942
|
+
window.location.reload();
|
|
34943
|
+
}
|
|
34944
|
+
return h("div", { style: { display: "flex", gap: 4, marginBottom: 24, borderBottom: "1px solid var(--border)", paddingBottom: 12, alignItems: "center" } },
|
|
34731
34945
|
props.tabs.map(function (t) {
|
|
34732
34946
|
var active = props.section[0] === t.id;
|
|
34733
34947
|
return h("button", {
|
|
@@ -34741,7 +34955,19 @@ export default async function handler(req, res) {
|
|
|
34741
34955
|
font: "inherit", fontWeight: 500, fontSize: 13, cursor: "pointer"
|
|
34742
34956
|
}
|
|
34743
34957
|
}, t.label);
|
|
34744
|
-
})
|
|
34958
|
+
}).concat([
|
|
34959
|
+
// Without this, a shared or public computer keeps a live admin session.
|
|
34960
|
+
h("button", {
|
|
34961
|
+
key: "__signout",
|
|
34962
|
+
onClick: signOut,
|
|
34963
|
+
title: "Sign out",
|
|
34964
|
+
style: {
|
|
34965
|
+
marginLeft: "auto", background: "transparent", color: "var(--muted)",
|
|
34966
|
+
border: "1px solid var(--border)", borderRadius: 999, padding: "8px 14px",
|
|
34967
|
+
font: "inherit", fontSize: 12, cursor: "pointer"
|
|
34968
|
+
}
|
|
34969
|
+
}, "Sign out")
|
|
34970
|
+
])
|
|
34745
34971
|
);
|
|
34746
34972
|
}
|
|
34747
34973
|
|
|
@@ -34768,8 +34994,11 @@ export default async function handler(req, res) {
|
|
|
34768
34994
|
var data = null;
|
|
34769
34995
|
try { data = await res.json(); } catch (_) { data = null; }
|
|
34770
34996
|
if (res.ok) {
|
|
34771
|
-
msg[1]("Password changed
|
|
34997
|
+
msg[1]("Password changed \u2014 signing you back in\u2026");
|
|
34772
34998
|
cur[1](""); next[1](""); again[1]("");
|
|
34999
|
+
// The server invalidated this session, so stay logged in would just
|
|
35000
|
+
// produce confusing 401s. Reload straight into the login screen.
|
|
35001
|
+
setTimeout(function () { window.location.reload(); }, 1200);
|
|
34773
35002
|
} else if (res.status === 401) {
|
|
34774
35003
|
err[1]((data && data.error) === "wrong_current_password"
|
|
34775
35004
|
? "That's not your current password."
|
|
@@ -80691,6 +80920,15 @@ var require_builder = __commonJS({
|
|
|
80691
80920
|
db2.run("UPDATE leads SET site_uid = ? WHERE id = ?", [uid, leadId]);
|
|
80692
80921
|
return uid;
|
|
80693
80922
|
}
|
|
80923
|
+
function reportKeyFor(leadId) {
|
|
80924
|
+
const db2 = (0, db_1.getDb)();
|
|
80925
|
+
const row = db2.get("SELECT report_key FROM leads WHERE id = ?", [leadId]);
|
|
80926
|
+
if (row?.report_key)
|
|
80927
|
+
return row.report_key;
|
|
80928
|
+
const key = (0, crypto_1.randomUUID)().replace(/-/g, "") + (0, crypto_1.randomUUID)().replace(/-/g, "");
|
|
80929
|
+
db2.run("UPDATE leads SET report_key = ? WHERE id = ?", [key, leadId]);
|
|
80930
|
+
return key;
|
|
80931
|
+
}
|
|
80694
80932
|
function authEncKeyFor(leadId) {
|
|
80695
80933
|
const db2 = (0, db_1.getDb)();
|
|
80696
80934
|
const row = db2.get("SELECT auth_enc_key FROM leads WHERE id = ?", [leadId]);
|
|
@@ -81377,7 +81615,9 @@ createRoot(document.getElementById('root')).render(<StrictMode><App /></StrictMo
|
|
|
81377
81615
|
version: "0.0.1",
|
|
81378
81616
|
type: "module",
|
|
81379
81617
|
scripts: { build: "vite build", dev: "vite" },
|
|
81380
|
-
|
|
81618
|
+
// @vercel/blob even without CMS: /api/track writes the visit counters, and
|
|
81619
|
+
// analytics ships on every site.
|
|
81620
|
+
dependencies: { ...baseDeps, "@vercel/blob": "^0.23.4" },
|
|
81381
81621
|
devDependencies: { "@vitejs/plugin-react": "^4.3.1", vite: "^5.4.2" }
|
|
81382
81622
|
});
|
|
81383
81623
|
const viteConfig = `import { defineConfig } from 'vite';
|
|
@@ -81457,6 +81697,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
81457
81697
|
ADMIN_PASSWORD_HASH: adminHash,
|
|
81458
81698
|
SESSION_SECRET: sessionSecret,
|
|
81459
81699
|
AUTH_ENC_KEY: authEncKeyFor(lead.id),
|
|
81700
|
+
REPORT_KEY: reportKeyFor(lead.id),
|
|
81460
81701
|
BUSINESS_NAME: lead.name
|
|
81461
81702
|
});
|
|
81462
81703
|
const vercelJson = JSON.stringify({
|
|
@@ -81471,23 +81712,39 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
81471
81712
|
{ file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" },
|
|
81472
81713
|
{ file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" },
|
|
81473
81714
|
{ file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" },
|
|
81474
|
-
{ file: "api/
|
|
81475
|
-
{ file: "api/stats.js", data: analytics_runtime_1.API_STATS_JS, encoding: "utf-8" },
|
|
81715
|
+
{ file: "api/admin/logout.js", data: cms_runtime_1.API_ADMIN_LOGOUT_JS, encoding: "utf-8" },
|
|
81476
81716
|
{ file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" },
|
|
81477
81717
|
{ file: "public/admin/index.html", data: adminHtml(), encoding: "utf-8" },
|
|
81478
81718
|
{ file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" }
|
|
81479
81719
|
];
|
|
81480
81720
|
cmsAdmin = { url: "", password: adminPassword };
|
|
81481
81721
|
}
|
|
81722
|
+
const analyticsFiles = [
|
|
81723
|
+
{ file: "api/track.js", data: analytics_runtime_1.API_TRACK_JS, encoding: "utf-8" },
|
|
81724
|
+
{ file: "api/stats.js", data: analytics_runtime_1.API_STATS_JS, encoding: "utf-8" }
|
|
81725
|
+
];
|
|
81482
81726
|
const allFiles = [
|
|
81483
81727
|
{ file: "index.html", data: indexHtml, encoding: "utf-8" },
|
|
81484
81728
|
{ file: "src/main.jsx", data: mainJsx, encoding: "utf-8" },
|
|
81485
81729
|
{ file: "src/App.jsx", data: appCode, encoding: "utf-8" },
|
|
81486
81730
|
{ file: "package.json", data: finalPackageJson, encoding: "utf-8" },
|
|
81487
81731
|
{ file: "vite.config.js", data: viteConfig, encoding: "utf-8" },
|
|
81732
|
+
...analyticsFiles,
|
|
81488
81733
|
...cmsExtraFiles,
|
|
81489
81734
|
...photoFiles
|
|
81490
81735
|
];
|
|
81736
|
+
if (!includeCms && blobToken) {
|
|
81737
|
+
onProgress("Setting analytics env vars\u2026");
|
|
81738
|
+
await setProjectEnv(projectName, vercelToken, {
|
|
81739
|
+
BLOB_READ_WRITE_TOKEN: blobToken,
|
|
81740
|
+
SITE_ID: siteUidFor(lead.id),
|
|
81741
|
+
BUSINESS_NAME: lead.name,
|
|
81742
|
+
// Encrypts stats.json — the Blob is public and its path is guessable, so
|
|
81743
|
+
// without this the visit counts are readable by anyone who finds the URL.
|
|
81744
|
+
AUTH_ENC_KEY: authEncKeyFor(lead.id),
|
|
81745
|
+
REPORT_KEY: reportKeyFor(lead.id)
|
|
81746
|
+
});
|
|
81747
|
+
}
|
|
81491
81748
|
const totalMb = allFiles.reduce((s, f) => s + f.data.length, 0) / 1048576;
|
|
81492
81749
|
onProgress(`Uploading ${allFiles.length} files (${totalMb.toFixed(1)} MB)\u2026`);
|
|
81493
81750
|
const res = await postVercelDeployment({
|
|
@@ -81570,7 +81827,7 @@ createRoot(document.getElementById('root')).render(<StrictMode><App /></StrictMo
|
|
|
81570
81827
|
const viteConfigCms = `import { defineConfig } from 'vite';
|
|
81571
81828
|
import react from '@vitejs/plugin-react';
|
|
81572
81829
|
export default defineConfig({ plugins: [react()] });`;
|
|
81573
|
-
filesToRedeploy.push({ file: "index.html", data: indexHtmlCms, encoding: "utf-8" }, { file: "src/main.jsx", data: mainJsxCms, encoding: "utf-8" }, { file: "vite.config.js", data: viteConfigCms, encoding: "utf-8" }, { file: "package.json", data: packageJsonCms, encoding: "utf-8" }, { file: "vercel.json", data: vercelJson, encoding: "utf-8" }, { file: "api/content.js", data: cms_runtime_1.API_CONTENT_JS, encoding: "utf-8" }, { file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" }, { file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" }, { file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" }, { file: "api/track.js", data: analytics_runtime_1.API_TRACK_JS, encoding: "utf-8" }, { file: "api/stats.js", data: analytics_runtime_1.API_STATS_JS, encoding: "utf-8" }, { file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" }, { file: "public/admin/index.html", data: adminHtml(), encoding: "utf-8" }, { file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" });
|
|
81830
|
+
filesToRedeploy.push({ file: "index.html", data: indexHtmlCms, encoding: "utf-8" }, { file: "src/main.jsx", data: mainJsxCms, encoding: "utf-8" }, { file: "vite.config.js", data: viteConfigCms, encoding: "utf-8" }, { file: "package.json", data: packageJsonCms, encoding: "utf-8" }, { file: "vercel.json", data: vercelJson, encoding: "utf-8" }, { file: "api/content.js", data: cms_runtime_1.API_CONTENT_JS, encoding: "utf-8" }, { file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" }, { file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" }, { file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" }, { file: "api/admin/logout.js", data: cms_runtime_1.API_ADMIN_LOGOUT_JS, encoding: "utf-8" }, { file: "api/track.js", data: analytics_runtime_1.API_TRACK_JS, encoding: "utf-8" }, { file: "api/stats.js", data: analytics_runtime_1.API_STATS_JS, encoding: "utf-8" }, { file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" }, { file: "public/admin/index.html", data: adminHtml(), encoding: "utf-8" }, { file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" });
|
|
81574
81831
|
if (!filesToRedeploy.some((f) => f.file === "src/App.jsx")) {
|
|
81575
81832
|
throw new Error("Couldn't fetch src/App.jsx from the existing deployment. Try Regenerate website first, then Redeploy CMS.");
|
|
81576
81833
|
}
|
|
@@ -81581,6 +81838,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
81581
81838
|
ADMIN_PASSWORD_HASH: adminHash,
|
|
81582
81839
|
SESSION_SECRET: sessionSecret,
|
|
81583
81840
|
AUTH_ENC_KEY: authEncKeyFor(lead.id),
|
|
81841
|
+
REPORT_KEY: reportKeyFor(lead.id),
|
|
81584
81842
|
BUSINESS_NAME: lead.name
|
|
81585
81843
|
};
|
|
81586
81844
|
if (existingUid)
|
|
@@ -81680,7 +81938,8 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
81680
81938
|
BLOB_READ_WRITE_TOKEN: blobToken,
|
|
81681
81939
|
ADMIN_PASSWORD_HASH: adminHash,
|
|
81682
81940
|
SESSION_SECRET: (0, cms_runtime_1.generateSessionSecret)(),
|
|
81683
|
-
AUTH_ENC_KEY: authEncKeyFor(lead.id)
|
|
81941
|
+
AUTH_ENC_KEY: authEncKeyFor(lead.id),
|
|
81942
|
+
REPORT_KEY: reportKeyFor(lead.id)
|
|
81684
81943
|
};
|
|
81685
81944
|
if (bookingExistingUid)
|
|
81686
81945
|
adminEnv.SITE_ID = bookingExistingUid;
|
|
@@ -81728,6 +81987,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
81728
81987
|
{ file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" },
|
|
81729
81988
|
{ file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" },
|
|
81730
81989
|
{ file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" },
|
|
81990
|
+
{ file: "api/admin/logout.js", data: cms_runtime_1.API_ADMIN_LOGOUT_JS, encoding: "utf-8" },
|
|
81731
81991
|
{ file: "api/track.js", data: analytics_runtime_1.API_TRACK_JS, encoding: "utf-8" },
|
|
81732
81992
|
{ file: "api/stats.js", data: analytics_runtime_1.API_STATS_JS, encoding: "utf-8" },
|
|
81733
81993
|
{ file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" }
|
|
@@ -81776,7 +82036,8 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
81776
82036
|
// Admin panel auth.
|
|
81777
82037
|
ADMIN_PASSWORD_HASH: adminHash,
|
|
81778
82038
|
SESSION_SECRET: (0, cms_runtime_1.generateSessionSecret)(),
|
|
81779
|
-
AUTH_ENC_KEY: authEncKeyFor(lead.id)
|
|
82039
|
+
AUTH_ENC_KEY: authEncKeyFor(lead.id),
|
|
82040
|
+
REPORT_KEY: reportKeyFor(lead.id)
|
|
81780
82041
|
});
|
|
81781
82042
|
const files = [
|
|
81782
82043
|
{ file: "index.html", data: booking_runtime_1.BOOK_INDEX_HTML, encoding: "utf-8" },
|
|
@@ -81790,6 +82051,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
81790
82051
|
{ file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" },
|
|
81791
82052
|
{ file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" },
|
|
81792
82053
|
{ file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" },
|
|
82054
|
+
{ file: "api/admin/logout.js", data: cms_runtime_1.API_ADMIN_LOGOUT_JS, encoding: "utf-8" },
|
|
81793
82055
|
{ file: "api/track.js", data: analytics_runtime_1.API_TRACK_JS, encoding: "utf-8" },
|
|
81794
82056
|
{ file: "api/stats.js", data: analytics_runtime_1.API_STATS_JS, encoding: "utf-8" },
|
|
81795
82057
|
// Serve /admin without a trailing-slash redirect dance.
|
|
@@ -81973,12 +82235,13 @@ export default defineConfig({ plugins: [react()] });`,
|
|
|
81973
82235
|
]
|
|
81974
82236
|
}),
|
|
81975
82237
|
encoding: "utf-8"
|
|
81976
|
-
}, { file: "api/content.js", data: cms_runtime_1.API_CONTENT_JS, encoding: "utf-8" }, { file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" }, { file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" }, { file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" }, { file: "api/track.js", data: analytics_runtime_1.API_TRACK_JS, encoding: "utf-8" }, { file: "api/stats.js", data: analytics_runtime_1.API_STATS_JS, encoding: "utf-8" }, { file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" }, { file: "public/admin/index.html", data: adminHtml(), encoding: "utf-8" }, { file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" });
|
|
82238
|
+
}, { file: "api/content.js", data: cms_runtime_1.API_CONTENT_JS, encoding: "utf-8" }, { file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" }, { file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" }, { file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" }, { file: "api/admin/logout.js", data: cms_runtime_1.API_ADMIN_LOGOUT_JS, encoding: "utf-8" }, { file: "api/track.js", data: analytics_runtime_1.API_TRACK_JS, encoding: "utf-8" }, { file: "api/stats.js", data: analytics_runtime_1.API_STATS_JS, encoding: "utf-8" }, { file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" }, { file: "public/admin/index.html", data: adminHtml(), encoding: "utf-8" }, { file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" });
|
|
81977
82239
|
const env = {
|
|
81978
82240
|
BLOB_READ_WRITE_TOKEN: blobToken,
|
|
81979
82241
|
ADMIN_PASSWORD_HASH: await (0, cms_runtime_1.hashPassword)(adminPassword),
|
|
81980
82242
|
SESSION_SECRET: (0, cms_runtime_1.generateSessionSecret)(),
|
|
81981
82243
|
AUTH_ENC_KEY: authEncKeyFor(lead.id),
|
|
82244
|
+
REPORT_KEY: reportKeyFor(lead.id),
|
|
81982
82245
|
BUSINESS_NAME: lead.name
|
|
81983
82246
|
};
|
|
81984
82247
|
if (siteUid)
|
|
@@ -83135,8 +83398,33 @@ var require_site_stats = __commonJS({
|
|
|
83135
83398
|
exports2.fetchSiteStats = fetchSiteStats;
|
|
83136
83399
|
exports2.pctChange = pctChange;
|
|
83137
83400
|
var axios_1 = __importDefault(require_axios());
|
|
83138
|
-
async function fetchSiteStats(siteUrl, adminPassword) {
|
|
83401
|
+
async function fetchSiteStats(siteUrl, adminPassword, reportKey) {
|
|
83139
83402
|
const base = siteUrl.replace(/\/$/, "");
|
|
83403
|
+
if (reportKey) {
|
|
83404
|
+
try {
|
|
83405
|
+
const res = await axios_1.default.get(`${base}/api/stats`, {
|
|
83406
|
+
timeout: 2e4,
|
|
83407
|
+
headers: { Authorization: `Bearer ${reportKey}` },
|
|
83408
|
+
validateStatus: () => true
|
|
83409
|
+
});
|
|
83410
|
+
if (res.status === 200)
|
|
83411
|
+
return { ok: true, data: res.data };
|
|
83412
|
+
if (res.status === 404) {
|
|
83413
|
+
return {
|
|
83414
|
+
ok: false,
|
|
83415
|
+
error: "This site was built before analytics shipped. Run `sproutkit regenerate` on it to add them."
|
|
83416
|
+
};
|
|
83417
|
+
}
|
|
83418
|
+
} catch (e) {
|
|
83419
|
+
return { ok: false, error: `Couldn't reach the site: ${e.message}` };
|
|
83420
|
+
}
|
|
83421
|
+
}
|
|
83422
|
+
if (!adminPassword) {
|
|
83423
|
+
return {
|
|
83424
|
+
ok: false,
|
|
83425
|
+
error: "No report key on this site and no stored admin password. Rebuild it (`sproutkit regenerate`) to provision a read-only report key."
|
|
83426
|
+
};
|
|
83427
|
+
}
|
|
83140
83428
|
let cookie;
|
|
83141
83429
|
try {
|
|
83142
83430
|
const login = await axios_1.default.post(`${base}/api/admin/login`, { password: adminPassword }, {
|
|
@@ -95933,20 +96221,22 @@ Interested?`;
|
|
|
95933
96221
|
}
|
|
95934
96222
|
case "site_report": {
|
|
95935
96223
|
const id = Number(input.id);
|
|
95936
|
-
const lead = db2.get(
|
|
96224
|
+
const lead = db2.get(`SELECT id, name, site_url, booking_url, admin_url, admin_password,
|
|
96225
|
+
booking_admin_password, report_key
|
|
96226
|
+
FROM leads WHERE id = ?`, [id]);
|
|
95937
96227
|
if (!lead)
|
|
95938
96228
|
throw new Error(`Lead #${id} not found`);
|
|
95939
96229
|
const base = lead.site_url || lead.booking_url;
|
|
95940
96230
|
if (!base)
|
|
95941
96231
|
return { ok: false, error: `#${id} ${lead.name} has no live site yet.` };
|
|
95942
96232
|
const password = lead.admin_password || lead.booking_admin_password;
|
|
95943
|
-
if (!password) {
|
|
96233
|
+
if (!password && !lead.report_key) {
|
|
95944
96234
|
return {
|
|
95945
96235
|
ok: false,
|
|
95946
|
-
error: `No admin password stored for #${id}, so I can't read its stats.
|
|
96236
|
+
error: `No report key or admin password stored for #${id}, so I can't read its stats. Run \`sproutkit regenerate ${id}\` to provision a read-only report key.`
|
|
95947
96237
|
};
|
|
95948
96238
|
}
|
|
95949
|
-
const stats = await (0, site_stats_1.fetchSiteStats)(base, password);
|
|
96239
|
+
const stats = await (0, site_stats_1.fetchSiteStats)(base, password, lead.report_key);
|
|
95950
96240
|
if (!stats.ok)
|
|
95951
96241
|
return { ok: false, error: stats.error, business: lead.name };
|
|
95952
96242
|
return { ok: true, id, business: lead.name, url: base, ...stats.data };
|
|
@@ -96984,7 +97274,7 @@ var require_package2 = __commonJS({
|
|
|
96984
97274
|
"package.json"(exports2, module2) {
|
|
96985
97275
|
module2.exports = {
|
|
96986
97276
|
name: "sproutkit-code",
|
|
96987
|
-
version: "1.
|
|
97277
|
+
version: "1.7.0",
|
|
96988
97278
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
96989
97279
|
bin: {
|
|
96990
97280
|
sproutkit: "./dist/index.js"
|