pagecast 0.1.1 → 0.1.2
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/feedback/worker.js +234 -0
- package/package.json +2 -1
- package/public/assets/{code-mirror-html-CwFouvGT.js → code-mirror-html-DFn3DH5h.js} +1 -1
- package/public/assets/{dnd-vendor-Cz0hTDTU.js → dnd-vendor-BmyNMol4.js} +1 -1
- package/public/assets/index-Ci9snOW8.css +1 -0
- package/public/assets/index-DThD-nOy.js +60 -0
- package/public/assets/motion-vendor-CqvjzA30.js +1 -0
- package/public/index.html +4 -4
- package/src/cli.js +49 -0
- package/src/server.js +322 -11
- package/public/assets/index-CKg3jtU_.js +0 -60
- package/public/assets/index-CfB-rxjb.css +0 -1
- package/public/assets/motion-vendor-CDgKLzlB.js +0 -9
package/src/server.js
CHANGED
|
@@ -26,6 +26,17 @@ export const DEFAULT_CLOUDFLARE_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
|
26
26
|
export const DEFAULT_CLOUDFLARE_LIST_TIMEOUT_MS = 60 * 1000;
|
|
27
27
|
export const CLOUDFLARE_OAUTH_SCOPES = ["account:read", "user:read", "pages:write"];
|
|
28
28
|
|
|
29
|
+
// Feedback provisioning deploys a Worker + KV, which the base publishing scopes
|
|
30
|
+
// don't permit. These elevate the grant only when the user opts into feedback,
|
|
31
|
+
// so publishing never has to request Workers/KV access up front.
|
|
32
|
+
export const FEEDBACK_OAUTH_SCOPES = [
|
|
33
|
+
"account:read",
|
|
34
|
+
"user:read",
|
|
35
|
+
"pages:write",
|
|
36
|
+
"workers_scripts:write",
|
|
37
|
+
"workers_kv:write"
|
|
38
|
+
];
|
|
39
|
+
|
|
29
40
|
const MIME_TYPES = new Map([
|
|
30
41
|
[".html", "text/html; charset=utf-8"],
|
|
31
42
|
[".htm", "text/html; charset=utf-8"],
|
|
@@ -228,6 +239,62 @@ function cleanCommandOutput(output) {
|
|
|
228
239
|
.trim();
|
|
229
240
|
}
|
|
230
241
|
|
|
242
|
+
// --- Feedback Worker provisioning: parse wrangler outputs --------------------
|
|
243
|
+
|
|
244
|
+
// A KV namespace id is a 32-char hex string. `wrangler kv namespace create`
|
|
245
|
+
// prints a TOML/JSON snippet containing `id = "<hex>"` / `"id": "<hex>"`.
|
|
246
|
+
export function parseKvNamespaceId(output) {
|
|
247
|
+
const text = stripAnsi(output || "");
|
|
248
|
+
const match = text.match(/(?:id\s*=\s*|"id"\s*:\s*)"([0-9a-f]{32})"/i);
|
|
249
|
+
return match ? match[1].toLowerCase() : "";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// `wrangler kv namespace list` prints a JSON array of { id, title }. Reuse an
|
|
253
|
+
// existing namespace by title so re-running setup is idempotent.
|
|
254
|
+
export function findKvNamespaceId(output, title) {
|
|
255
|
+
const text = stripAnsi(output || "");
|
|
256
|
+
try {
|
|
257
|
+
const start = text.indexOf("[");
|
|
258
|
+
const end = text.lastIndexOf("]");
|
|
259
|
+
if (start >= 0 && end > start) {
|
|
260
|
+
const list = JSON.parse(text.slice(start, end + 1));
|
|
261
|
+
const hit = list.find((entry) => entry && entry.title === title);
|
|
262
|
+
if (hit && /^[0-9a-f]{32}$/i.test(hit.id || "")) {
|
|
263
|
+
return String(hit.id).toLowerCase();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
// Non-JSON or unexpected shape — treat as "not found" and create one.
|
|
268
|
+
}
|
|
269
|
+
return "";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// The deployed Worker's public origin, e.g. https://name.sub.workers.dev.
|
|
273
|
+
export function parseWorkerDevUrl(output) {
|
|
274
|
+
const text = stripAnsi(output || "");
|
|
275
|
+
const match = text.match(/https:\/\/[a-z0-9-]+\.[a-z0-9-]+\.workers\.dev/i);
|
|
276
|
+
return match ? match[0].toLowerCase() : "";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Normalize the persisted feedback (reactions + analytics) settings. Returns
|
|
280
|
+
// null until the feedback Worker has been provisioned, so callers can treat the
|
|
281
|
+
// whole feature as off by checking for a truthy `feedback`.
|
|
282
|
+
function normalizeFeedback(feedback) {
|
|
283
|
+
if (!feedback || typeof feedback !== "object") {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const url = String(feedback.url || "").trim().replace(/\/+$/, "");
|
|
287
|
+
if (!/^https:\/\/[^\s/]+/i.test(url)) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
url,
|
|
292
|
+
statsToken: String(feedback.statsToken || ""),
|
|
293
|
+
workerName: String(feedback.workerName || ""),
|
|
294
|
+
kvId: String(feedback.kvId || "")
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
231
298
|
function normalizeConfig(config = {}) {
|
|
232
299
|
const projectName = normalizePagesProjectName(
|
|
233
300
|
config.pages?.projectName || DEFAULT_PAGES_PROJECT_NAME
|
|
@@ -242,7 +309,8 @@ function normalizeConfig(config = {}) {
|
|
|
242
309
|
accountName,
|
|
243
310
|
branch: DEFAULT_PAGES_BRANCH,
|
|
244
311
|
baseUrl: pagesBaseUrl(projectName)
|
|
245
|
-
}
|
|
312
|
+
},
|
|
313
|
+
feedback: normalizeFeedback(config.feedback)
|
|
246
314
|
};
|
|
247
315
|
}
|
|
248
316
|
|
|
@@ -889,7 +957,19 @@ export function createConfigStore({ dataDir = path.join(PROJECT_ROOT, ".pagecast
|
|
|
889
957
|
projectName: projectName === undefined ? config.pages.projectName : projectName,
|
|
890
958
|
accountId: nextAccountId,
|
|
891
959
|
accountName: nextAccountName
|
|
892
|
-
}
|
|
960
|
+
},
|
|
961
|
+
// Preserve feedback config — a pages update (e.g. persisting the account on
|
|
962
|
+
// publish) must not wipe an already-provisioned feedback Worker.
|
|
963
|
+
feedback: config.feedback
|
|
964
|
+
});
|
|
965
|
+
await save();
|
|
966
|
+
return get();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async function updateFeedback(feedback) {
|
|
970
|
+
config = normalizeConfig({
|
|
971
|
+
pages: config.pages,
|
|
972
|
+
feedback: feedback === null ? null : { ...(config.feedback || {}), ...feedback }
|
|
893
973
|
});
|
|
894
974
|
await save();
|
|
895
975
|
return get();
|
|
@@ -899,6 +979,7 @@ export function createConfigStore({ dataDir = path.join(PROJECT_ROOT, ".pagecast
|
|
|
899
979
|
init,
|
|
900
980
|
get,
|
|
901
981
|
updatePages,
|
|
982
|
+
updateFeedback,
|
|
902
983
|
configPath
|
|
903
984
|
};
|
|
904
985
|
}
|
|
@@ -923,11 +1004,41 @@ export function createDeployQueue() {
|
|
|
923
1004
|
return { enqueue };
|
|
924
1005
|
}
|
|
925
1006
|
|
|
1007
|
+
// Insert the feedback widget into a published HTML document. The widget (served
|
|
1008
|
+
// by the user's feedback Worker) beacons a view and renders the reactions bar.
|
|
1009
|
+
// Injected just before </body> so it loads after page content. `url` is the
|
|
1010
|
+
// Worker origin and `slug` keys this page's stats. Returns the HTML unchanged
|
|
1011
|
+
// when feedback is not configured. Pure + exported for testing.
|
|
1012
|
+
export function injectFeedbackWidget(html, { url, slug } = {}) {
|
|
1013
|
+
const baseUrl = String(url || "").trim().replace(/\/+$/, "");
|
|
1014
|
+
const pageSlug = String(slug || "").trim();
|
|
1015
|
+
if (!baseUrl || !pageSlug) {
|
|
1016
|
+
return html;
|
|
1017
|
+
}
|
|
1018
|
+
const esc = (value) =>
|
|
1019
|
+
String(value)
|
|
1020
|
+
.replace(/&/g, "&")
|
|
1021
|
+
.replace(/"/g, """)
|
|
1022
|
+
.replace(/</g, "<")
|
|
1023
|
+
.replace(/>/g, ">");
|
|
1024
|
+
const tag =
|
|
1025
|
+
`<script src="${esc(`${baseUrl}/widget.js`)}" data-slug="${esc(pageSlug)}" defer></script>`;
|
|
1026
|
+
// Avoid double-injecting if the document already carries the widget.
|
|
1027
|
+
if (html.includes(`data-slug="${esc(pageSlug)}"`) && html.includes("/widget.js")) {
|
|
1028
|
+
return html;
|
|
1029
|
+
}
|
|
1030
|
+
if (/<\/body>/i.test(html)) {
|
|
1031
|
+
return html.replace(/<\/body>/i, `${tag}\n</body>`);
|
|
1032
|
+
}
|
|
1033
|
+
return `${html}\n${tag}\n`;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
926
1036
|
export function createCloudflarePagesPublisher({
|
|
927
1037
|
dataDir = path.join(PROJECT_ROOT, ".pagecast"),
|
|
928
1038
|
spawnImpl = spawn,
|
|
929
1039
|
timeoutMs = 180000,
|
|
930
|
-
getRedirects = () => []
|
|
1040
|
+
getRedirects = () => [],
|
|
1041
|
+
getFeedback = () => null
|
|
931
1042
|
} = {}) {
|
|
932
1043
|
const siteRoot = path.join(dataDir, "pages-site");
|
|
933
1044
|
const deployRoot = path.join(dataDir, "pages-deploy");
|
|
@@ -966,18 +1077,28 @@ export function createCloudflarePagesPublisher({
|
|
|
966
1077
|
}
|
|
967
1078
|
|
|
968
1079
|
async function stagePublication(report, publication) {
|
|
969
|
-
const
|
|
1080
|
+
const slug = publication.slug || publication.token;
|
|
1081
|
+
const destinationRoot = publicationDir(slug);
|
|
970
1082
|
const sourceRoot = publishSourceFor(report);
|
|
971
1083
|
await copyPublicTree(sourceRoot, destinationRoot);
|
|
1084
|
+
|
|
1085
|
+
const indexPath = path.join(destinationRoot, "index.html");
|
|
1086
|
+
let html;
|
|
972
1087
|
if (isMarkdownFileName(report.entryFile)) {
|
|
973
1088
|
// Render the raw markdown entry to real HTML so the published Cloudflare
|
|
974
1089
|
// site serves a proper document; sibling assets were copied above.
|
|
975
1090
|
const markdown = await fs.readFile(path.join(sourceRoot, report.entryFile), "utf8");
|
|
976
|
-
|
|
977
|
-
await fs.writeFile(path.join(destinationRoot, "index.html"), html, "utf8");
|
|
1091
|
+
html = markdownToHtml(markdown, { title: report.name });
|
|
978
1092
|
} else {
|
|
979
|
-
await fs.
|
|
1093
|
+
html = await fs.readFile(path.join(sourceRoot, report.entryFile), "utf8");
|
|
980
1094
|
}
|
|
1095
|
+
|
|
1096
|
+
// Inject the reactions + analytics widget when feedback is provisioned.
|
|
1097
|
+
const feedback = getFeedback();
|
|
1098
|
+
if (feedback?.url) {
|
|
1099
|
+
html = injectFeedbackWidget(html, { url: feedback.url, slug });
|
|
1100
|
+
}
|
|
1101
|
+
await fs.writeFile(indexPath, html, "utf8");
|
|
981
1102
|
}
|
|
982
1103
|
|
|
983
1104
|
async function removePublication(slug) {
|
|
@@ -1258,8 +1379,8 @@ export function createCloudflareAuthManager({
|
|
|
1258
1379
|
// connect/refresh time; the cache is invalidated whenever login state changes.
|
|
1259
1380
|
let sessionCache = null;
|
|
1260
1381
|
|
|
1261
|
-
async function login() {
|
|
1262
|
-
const scopedArgs =
|
|
1382
|
+
async function login(scopes = CLOUDFLARE_OAUTH_SCOPES) {
|
|
1383
|
+
const scopedArgs = scopes.flatMap((scope) => ["--scopes", scope]);
|
|
1263
1384
|
await runWrangler(["login", ...scopedArgs], loginTimeoutMs);
|
|
1264
1385
|
sessionCache = null;
|
|
1265
1386
|
}
|
|
@@ -1342,6 +1463,98 @@ export function createCloudflareAuthManager({
|
|
|
1342
1463
|
return name;
|
|
1343
1464
|
}
|
|
1344
1465
|
|
|
1466
|
+
// Provision the feedback Worker: reuse-or-create a KV namespace, stage the
|
|
1467
|
+
// worker + a generated wrangler.toml, and deploy it to the account's
|
|
1468
|
+
// workers.dev. Returns { url, kvId, workerName, statsToken }. Side-effecting
|
|
1469
|
+
// (creates real Cloudflare resources) — only run on explicit user action.
|
|
1470
|
+
async function setupFeedback({
|
|
1471
|
+
accountId = "",
|
|
1472
|
+
workerName = "pagecast-feedback",
|
|
1473
|
+
workerSource = "",
|
|
1474
|
+
statsToken = "",
|
|
1475
|
+
deployDir,
|
|
1476
|
+
timeoutMs = 120000
|
|
1477
|
+
} = {}) {
|
|
1478
|
+
if (!workerSource) {
|
|
1479
|
+
throw appError("Feedback Worker source was not found in the package.", 500);
|
|
1480
|
+
}
|
|
1481
|
+
if (!deployDir) {
|
|
1482
|
+
throw appError("A deploy directory is required to set up feedback.", 500);
|
|
1483
|
+
}
|
|
1484
|
+
const env = accountId ? { CLOUDFLARE_ACCOUNT_ID: accountId } : {};
|
|
1485
|
+
const kvTitle = `${workerName}-store`;
|
|
1486
|
+
|
|
1487
|
+
const provision = async () => {
|
|
1488
|
+
// 1. Reuse or create the KV namespace.
|
|
1489
|
+
let kvId = "";
|
|
1490
|
+
try {
|
|
1491
|
+
const listOut = await runWrangler(["kv", "namespace", "list"], timeoutMs, env);
|
|
1492
|
+
kvId = findKvNamespaceId(listOut, kvTitle);
|
|
1493
|
+
} catch {
|
|
1494
|
+
// Listing isn't available/authorized — fall through to create.
|
|
1495
|
+
}
|
|
1496
|
+
if (!kvId) {
|
|
1497
|
+
const createOut = await runWrangler(
|
|
1498
|
+
["kv", "namespace", "create", kvTitle],
|
|
1499
|
+
timeoutMs,
|
|
1500
|
+
env
|
|
1501
|
+
);
|
|
1502
|
+
kvId = parseKvNamespaceId(createOut);
|
|
1503
|
+
}
|
|
1504
|
+
if (!kvId) {
|
|
1505
|
+
throw appError("Could not create the feedback KV namespace.", 502);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// 2. Stage worker.js + a generated wrangler.toml in a clean temp dir.
|
|
1509
|
+
await fs.rm(deployDir, { recursive: true, force: true });
|
|
1510
|
+
await fs.mkdir(deployDir, { recursive: true });
|
|
1511
|
+
await fs.writeFile(path.join(deployDir, "worker.js"), workerSource, "utf8");
|
|
1512
|
+
const toml = [
|
|
1513
|
+
`name = "${workerName}"`,
|
|
1514
|
+
`main = "worker.js"`,
|
|
1515
|
+
`compatibility_date = "2024-09-01"`,
|
|
1516
|
+
`workers_dev = true`,
|
|
1517
|
+
``,
|
|
1518
|
+
`[[kv_namespaces]]`,
|
|
1519
|
+
`binding = "PAGECAST_FEEDBACK"`,
|
|
1520
|
+
`id = "${kvId}"`,
|
|
1521
|
+
``,
|
|
1522
|
+
`[vars]`,
|
|
1523
|
+
`PAGECAST_STATS_TOKEN = "${statsToken}"`,
|
|
1524
|
+
``
|
|
1525
|
+
].join("\n");
|
|
1526
|
+
await fs.writeFile(path.join(deployDir, "wrangler.toml"), toml, "utf8");
|
|
1527
|
+
|
|
1528
|
+
// 3. Deploy. Wrangler resolves `main` relative to the config file's dir.
|
|
1529
|
+
const deployOut = await runWrangler(
|
|
1530
|
+
["deploy", "--config", path.join(deployDir, "wrangler.toml")],
|
|
1531
|
+
timeoutMs,
|
|
1532
|
+
env
|
|
1533
|
+
);
|
|
1534
|
+
const url = parseWorkerDevUrl(deployOut);
|
|
1535
|
+
if (!url) {
|
|
1536
|
+
throw appError(
|
|
1537
|
+
"Feedback Worker deployed but no workers.dev URL was returned. Enable a workers.dev subdomain in your Cloudflare dashboard, then retry.",
|
|
1538
|
+
502
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
return { url, kvId, workerName, statsToken };
|
|
1542
|
+
};
|
|
1543
|
+
|
|
1544
|
+
try {
|
|
1545
|
+
return await provision();
|
|
1546
|
+
} catch (error) {
|
|
1547
|
+
// The base publishing OAuth lacks Workers/KV permission, surfacing as a
|
|
1548
|
+
// Cloudflare "Authentication error [code: 10000]". Elevate the grant to the
|
|
1549
|
+
// feedback scopes once, then retry. (No-op if a token is already broad.)
|
|
1550
|
+
if (/code:\s*10000|authentication error/i.test(stripAnsi(error.message || ""))) {
|
|
1551
|
+
await login(FEEDBACK_OAUTH_SCOPES);
|
|
1552
|
+
return await provision();
|
|
1553
|
+
}
|
|
1554
|
+
throw error;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1345
1558
|
function cachedSession() {
|
|
1346
1559
|
return sessionCache ? sessionCache.value : { loggedIn: false, accounts: [] };
|
|
1347
1560
|
}
|
|
@@ -1369,6 +1582,7 @@ export function createCloudflareAuthManager({
|
|
|
1369
1582
|
loginAndListProjects,
|
|
1370
1583
|
whoami,
|
|
1371
1584
|
ensureProject,
|
|
1585
|
+
setupFeedback,
|
|
1372
1586
|
cachedSession,
|
|
1373
1587
|
refreshSession,
|
|
1374
1588
|
invalidateSession
|
|
@@ -3165,6 +3379,60 @@ async function handleApi(
|
|
|
3165
3379
|
return;
|
|
3166
3380
|
}
|
|
3167
3381
|
|
|
3382
|
+
// Provision (or re-provision) the feedback Worker + KV on the user's account.
|
|
3383
|
+
// Creates real Cloudflare resources, so it only runs on this explicit action.
|
|
3384
|
+
if (url.pathname === "/api/feedback/setup" && req.method === "POST") {
|
|
3385
|
+
const body = await readJsonBody(req);
|
|
3386
|
+
const pages = configStore.get().pages;
|
|
3387
|
+
const accountId = normalizeAccountId(body.accountId || pages.accountId || "");
|
|
3388
|
+
const workerPath = path.join(PROJECT_ROOT, "feedback", "worker.js");
|
|
3389
|
+
let workerSource;
|
|
3390
|
+
try {
|
|
3391
|
+
workerSource = await fs.readFile(workerPath, "utf8");
|
|
3392
|
+
} catch {
|
|
3393
|
+
sendError(res, appError("Feedback Worker source not found in the package.", 500));
|
|
3394
|
+
return;
|
|
3395
|
+
}
|
|
3396
|
+
const existing = configStore.get().feedback;
|
|
3397
|
+
const statsToken = existing?.statsToken || randomBytes(24).toString("hex");
|
|
3398
|
+
const dataDir = path.dirname(configStore.configPath);
|
|
3399
|
+
try {
|
|
3400
|
+
const result = await cloudflareAuth.setupFeedback({
|
|
3401
|
+
accountId,
|
|
3402
|
+
workerSource,
|
|
3403
|
+
statsToken,
|
|
3404
|
+
deployDir: path.join(dataDir, "feedback-deploy")
|
|
3405
|
+
});
|
|
3406
|
+
const config = await configStore.updateFeedback(result);
|
|
3407
|
+
sendJson(res, 200, { config, feedback: config.feedback });
|
|
3408
|
+
} catch (error) {
|
|
3409
|
+
sendError(res, error);
|
|
3410
|
+
}
|
|
3411
|
+
return;
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
// Read aggregate stats for a published page back from the feedback Worker.
|
|
3415
|
+
// Proxied through the local server so the stats token never reaches the UI.
|
|
3416
|
+
if (url.pathname === "/api/feedback/stats" && req.method === "GET") {
|
|
3417
|
+
const feedback = configStore.get().feedback;
|
|
3418
|
+
if (!feedback?.url) {
|
|
3419
|
+
sendJson(res, 200, { ok: true, configured: false, stats: null });
|
|
3420
|
+
return;
|
|
3421
|
+
}
|
|
3422
|
+
const slug = url.searchParams.get("slug") || "";
|
|
3423
|
+
const statsUrl =
|
|
3424
|
+
`${feedback.url}/api/v1/stats?slug=${encodeURIComponent(slug)}` +
|
|
3425
|
+
`&token=${encodeURIComponent(feedback.statsToken)}`;
|
|
3426
|
+
try {
|
|
3427
|
+
const response = await fetch(statsUrl);
|
|
3428
|
+
const data = await response.json().catch(() => ({}));
|
|
3429
|
+
sendJson(res, 200, { ok: response.ok, configured: true, ...data });
|
|
3430
|
+
} catch {
|
|
3431
|
+
sendError(res, appError("Could not reach the feedback service.", 502));
|
|
3432
|
+
}
|
|
3433
|
+
return;
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3168
3436
|
if (url.pathname === "/api/cloudflare/login" && req.method === "POST") {
|
|
3169
3437
|
await readJsonBody(req);
|
|
3170
3438
|
await cloudflareAuth.login();
|
|
@@ -3593,7 +3861,8 @@ export async function startServers({
|
|
|
3593
3861
|
dataDir,
|
|
3594
3862
|
spawnImpl: pagesDeploySpawnImpl,
|
|
3595
3863
|
timeoutMs: pagesDeployTimeoutMs,
|
|
3596
|
-
getRedirects: () => store.listRedirects()
|
|
3864
|
+
getRedirects: () => store.listRedirects(),
|
|
3865
|
+
getFeedback: () => configStore.get().feedback
|
|
3597
3866
|
});
|
|
3598
3867
|
const deployQueue = createDeployQueue();
|
|
3599
3868
|
const watchManager = createWatchManager({
|
|
@@ -3685,7 +3954,10 @@ async function createHeadlessCloudflareContext({
|
|
|
3685
3954
|
const pagesPublisher = createCloudflarePagesPublisher({
|
|
3686
3955
|
dataDir,
|
|
3687
3956
|
spawnImpl: pagesDeploySpawnImpl,
|
|
3688
|
-
timeoutMs: pagesDeployTimeoutMs
|
|
3957
|
+
timeoutMs: pagesDeployTimeoutMs,
|
|
3958
|
+
// Headless/CLI publishes (incl. the agent skill's `npx pagecast publish`)
|
|
3959
|
+
// must inject the feedback widget too, not just the running app.
|
|
3960
|
+
getFeedback: () => configStore.get().feedback
|
|
3689
3961
|
});
|
|
3690
3962
|
return { configStore, cloudflareAuth, pagesPublisher };
|
|
3691
3963
|
}
|
|
@@ -3778,6 +4050,45 @@ export async function setupCloudflarePages({
|
|
|
3778
4050
|
};
|
|
3779
4051
|
}
|
|
3780
4052
|
|
|
4053
|
+
// Provision the feedback Worker + KV on the user's account and persist the
|
|
4054
|
+
// resulting config. Reuses an existing stats token/namespace on re-run.
|
|
4055
|
+
export async function setupCloudflareFeedback({
|
|
4056
|
+
accountId,
|
|
4057
|
+
dataDir = path.join(PROJECT_ROOT, ".pagecast"),
|
|
4058
|
+
cloudflareAuthSpawnImpl = spawn,
|
|
4059
|
+
cloudflareListTimeoutMs = DEFAULT_CLOUDFLARE_LIST_TIMEOUT_MS,
|
|
4060
|
+
feedbackTimeoutMs = 120000
|
|
4061
|
+
} = {}) {
|
|
4062
|
+
const { configStore, cloudflareAuth } = await createHeadlessCloudflareContext({
|
|
4063
|
+
dataDir,
|
|
4064
|
+
cloudflareAuthSpawnImpl,
|
|
4065
|
+
cloudflareListTimeoutMs
|
|
4066
|
+
});
|
|
4067
|
+
const pages = configStore.get().pages;
|
|
4068
|
+
const resolvedAccountId = normalizeAccountId(accountId || pages.accountId || "");
|
|
4069
|
+
|
|
4070
|
+
const workerPath = path.join(PROJECT_ROOT, "feedback", "worker.js");
|
|
4071
|
+
let workerSource;
|
|
4072
|
+
try {
|
|
4073
|
+
workerSource = await fs.readFile(workerPath, "utf8");
|
|
4074
|
+
} catch {
|
|
4075
|
+
throw appError("Feedback Worker source not found in the package.", 500);
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
const existing = configStore.get().feedback;
|
|
4079
|
+
const statsToken = existing?.statsToken || randomBytes(24).toString("hex");
|
|
4080
|
+
|
|
4081
|
+
const result = await cloudflareAuth.setupFeedback({
|
|
4082
|
+
accountId: resolvedAccountId,
|
|
4083
|
+
workerSource,
|
|
4084
|
+
statsToken,
|
|
4085
|
+
deployDir: path.join(dataDir, "feedback-deploy")
|
|
4086
|
+
});
|
|
4087
|
+
|
|
4088
|
+
const config = await configStore.updateFeedback(result);
|
|
4089
|
+
return { config, feedback: config.feedback };
|
|
4090
|
+
}
|
|
4091
|
+
|
|
3781
4092
|
export async function getCloudflarePagesStatus({
|
|
3782
4093
|
dataDir = path.join(PROJECT_ROOT, ".pagecast"),
|
|
3783
4094
|
cloudflareAuthSpawnImpl = spawn,
|