@aeptus/aexeo-emdash 0.8.18 → 0.8.19
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/CHANGELOG.md +104 -6
- package/INSTALL.md +13 -4
- package/LICENSE +0 -0
- package/README.md +0 -0
- package/dist/adapter.d.ts +0 -0
- package/dist/adapter.d.ts.map +0 -0
- package/dist/admin/Absorbability.d.ts +0 -0
- package/dist/admin/Absorbability.d.ts.map +0 -0
- package/dist/admin/Accessibility.d.ts +0 -0
- package/dist/admin/Accessibility.d.ts.map +0 -0
- package/dist/admin/Citability.d.ts +0 -0
- package/dist/admin/Citability.d.ts.map +0 -0
- package/dist/admin/EntityLegitimacy.d.ts +0 -0
- package/dist/admin/EntityLegitimacy.d.ts.map +0 -0
- package/dist/admin/EntityPresence.d.ts +0 -0
- package/dist/admin/EntityPresence.d.ts.map +0 -0
- package/dist/admin/Facts.d.ts +0 -0
- package/dist/admin/Facts.d.ts.map +0 -0
- package/dist/admin/Findings.d.ts +0 -0
- package/dist/admin/Findings.d.ts.map +0 -0
- package/dist/admin/PillarView.d.ts +0 -0
- package/dist/admin/PillarView.d.ts.map +0 -0
- package/dist/admin/Retrievability.d.ts +0 -0
- package/dist/admin/Retrievability.d.ts.map +0 -0
- package/dist/admin.d.ts +0 -0
- package/dist/admin.d.ts.map +0 -0
- package/dist/admin.js +0 -0
- package/dist/configured.d.ts +0 -0
- package/dist/configured.d.ts.map +1 -1
- package/dist/configured.js +331 -283
- package/dist/data-route.d.ts +0 -0
- package/dist/data-route.d.ts.map +0 -0
- package/dist/evaluator-sandbox.d.ts +0 -0
- package/dist/evaluator-sandbox.d.ts.map +0 -0
- package/dist/evaluator.d.ts +0 -0
- package/dist/evaluator.d.ts.map +0 -0
- package/dist/facts-route.d.ts +0 -0
- package/dist/facts-route.d.ts.map +0 -0
- package/dist/findings-view.d.ts +136 -0
- package/dist/findings-view.d.ts.map +1 -0
- package/dist/index.d.ts +0 -0
- package/dist/index.d.ts.map +0 -0
- package/dist/index.js +13 -2
- package/dist/plugin.d.ts +22 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/presence-check.d.ts +0 -0
- package/dist/presence-check.d.ts.map +0 -0
- package/dist/presence-route.d.ts +0 -0
- package/dist/presence-route.d.ts.map +0 -0
- package/dist/sandbox-entry.d.ts +2 -22
- package/dist/sandbox-entry.d.ts.map +1 -1
- package/dist/sandbox-entry.js +410 -353
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sidecar-url.d.ts +18 -0
- package/dist/sidecar-url.d.ts.map +1 -0
- package/dist/sidecar.d.ts +0 -0
- package/dist/sidecar.d.ts.map +0 -0
- package/dist/suppressions.d.ts +0 -0
- package/dist/suppressions.d.ts.map +0 -0
- package/dist/types.d.ts +0 -0
- package/dist/types.d.ts.map +0 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +0 -0
- package/dist/wasm-init.d.ts +0 -0
- package/dist/wasm-init.d.ts.map +0 -0
- package/package.json +4 -2
- package/wasm/aexeo_emdash_bridge.d.ts +0 -0
- package/wasm/aexeo_emdash_bridge.js +0 -0
- package/wasm/aexeo_emdash_bridge_bg.d.ts +0 -0
- package/wasm/aexeo_emdash_bridge_bg.js +0 -0
- package/wasm/aexeo_emdash_bridge_bg.wasm +0 -0
- package/wasm/aexeo_emdash_bridge_bg.wasm.d.ts +0 -0
- package/dist/indexnow.d.ts +0 -15
- package/dist/indexnow.d.ts.map +0 -1
- package/dist/mcp.d.ts +0 -28
- package/dist/mcp.d.ts.map +0 -1
package/dist/sandbox-entry.js
CHANGED
|
@@ -255,11 +255,27 @@ function trimTrailingSlash(url) {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
// src/plugin.ts
|
|
258
|
+
function buildAllowedHosts(evaluatorHost) {
|
|
259
|
+
const host = normalizeHost(evaluatorHost);
|
|
260
|
+
return host === null ? [] : [host];
|
|
261
|
+
}
|
|
262
|
+
function normalizeHost(evaluatorHost) {
|
|
263
|
+
const host = evaluatorHost?.trim() ?? "";
|
|
264
|
+
return host.length === 0 ? null : host;
|
|
265
|
+
}
|
|
258
266
|
var CONFIG_URL_KEY = "config:evaluator_url";
|
|
259
267
|
var CONFIG_TOKEN_KEY = "config:eval_token";
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
268
|
+
var SETTINGS_TOKEN_KEY = "eval_token";
|
|
269
|
+
var PLUGIN_SETTINGS_SCHEMA = {
|
|
270
|
+
[SETTINGS_TOKEN_KEY]: {
|
|
271
|
+
type: "secret",
|
|
272
|
+
label: "EVAL_TOKEN",
|
|
273
|
+
description: "Bearer token for your aexeo-crawl-worker Worker. Encrypted at rest by the emdash host."
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
async function readSidecarConfig(store) {
|
|
277
|
+
const url = await store.kv.get(CONFIG_URL_KEY);
|
|
278
|
+
const token = await readStoredToken(store);
|
|
263
279
|
if (typeof url !== "string" || typeof token !== "string") {
|
|
264
280
|
return null;
|
|
265
281
|
}
|
|
@@ -268,12 +284,48 @@ async function readSidecarConfig(kv) {
|
|
|
268
284
|
}
|
|
269
285
|
return { url, token };
|
|
270
286
|
}
|
|
271
|
-
async function writeSidecarConfig(
|
|
272
|
-
await kv.set(CONFIG_URL_KEY, config.url);
|
|
273
|
-
await
|
|
287
|
+
async function writeSidecarConfig(store, config) {
|
|
288
|
+
await store.kv.set(CONFIG_URL_KEY, config.url);
|
|
289
|
+
await writeStoredToken(store, config.token);
|
|
290
|
+
}
|
|
291
|
+
async function readStoredToken(store) {
|
|
292
|
+
const { settings } = store;
|
|
293
|
+
if (settings !== void 0) {
|
|
294
|
+
try {
|
|
295
|
+
const stored = await settings.get(SETTINGS_TOKEN_KEY);
|
|
296
|
+
if (typeof stored === "string" && stored.length > 0) {
|
|
297
|
+
return stored;
|
|
298
|
+
}
|
|
299
|
+
} catch (err) {
|
|
300
|
+
store.log?.warn?.(
|
|
301
|
+
`aexeo: could not read the encrypted EVAL_TOKEN from plugin settings, falling back to KV: ${describeError(err, "")}`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const legacy = await store.kv.get(CONFIG_TOKEN_KEY);
|
|
306
|
+
return typeof legacy === "string" && legacy.length > 0 ? legacy : null;
|
|
307
|
+
}
|
|
308
|
+
async function writeStoredToken(store, token) {
|
|
309
|
+
const { settings } = store;
|
|
310
|
+
if (settings !== void 0) {
|
|
311
|
+
try {
|
|
312
|
+
await settings.set(SETTINGS_TOKEN_KEY, token);
|
|
313
|
+
await store.kv.delete(CONFIG_TOKEN_KEY);
|
|
314
|
+
return;
|
|
315
|
+
} catch (err) {
|
|
316
|
+
store.log?.warn?.(
|
|
317
|
+
`aexeo: could not store the EVAL_TOKEN encrypted in plugin settings; it will be written to plugin KV in plaintext instead. Set EMDASH_ENCRYPTION_KEY on the emdash host to fix this. Detail: ${describeError(err, token)}`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
await store.kv.set(CONFIG_TOKEN_KEY, token);
|
|
322
|
+
}
|
|
323
|
+
function describeError(err, secret) {
|
|
324
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
325
|
+
return secret.length === 0 ? raw : raw.split(secret).join("[redacted]");
|
|
274
326
|
}
|
|
275
327
|
async function handleAfterSave(event, ctx) {
|
|
276
|
-
const runtime = await readSidecarConfig(ctx
|
|
328
|
+
const runtime = await readSidecarConfig(ctx);
|
|
277
329
|
if (runtime === null) {
|
|
278
330
|
ctx.log?.warn?.(
|
|
279
331
|
"Aexeo afterSave skipped: configure the evaluator URL and token on the Setup page"
|
|
@@ -378,18 +430,18 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
378
430
|
summary.errors.push(`${result.reason}: ${result.detail}`);
|
|
379
431
|
return summary;
|
|
380
432
|
}
|
|
381
|
-
const
|
|
433
|
+
const findingsByRoute = /* @__PURE__ */ new Map();
|
|
382
434
|
for (const route of documentRoutes) {
|
|
383
|
-
|
|
435
|
+
findingsByRoute.set(route, []);
|
|
384
436
|
}
|
|
385
437
|
for (const finding of result.findings) {
|
|
386
438
|
const bucket = finding.scope === "page" ? finding.path : "*";
|
|
387
|
-
const list =
|
|
439
|
+
const list = findingsByRoute.get(bucket) ?? [];
|
|
388
440
|
list.push(finding);
|
|
389
|
-
|
|
441
|
+
findingsByRoute.set(bucket, list);
|
|
390
442
|
}
|
|
391
443
|
let totalAfterSuppression = 0;
|
|
392
|
-
for (const [route, findings] of
|
|
444
|
+
for (const [route, findings] of findingsByRoute) {
|
|
393
445
|
const stored = adaptedByRoute.get(route);
|
|
394
446
|
const context = {
|
|
395
447
|
route,
|
|
@@ -481,10 +533,100 @@ async function readFindings(kv, route) {
|
|
|
481
533
|
return stored.findings;
|
|
482
534
|
}
|
|
483
535
|
|
|
484
|
-
// src/
|
|
485
|
-
|
|
486
|
-
|
|
536
|
+
// src/sidecar-url.ts
|
|
537
|
+
function validateSidecarUrl(raw, allowedHosts = []) {
|
|
538
|
+
const url = raw.trim();
|
|
539
|
+
if (url.length === 0) {
|
|
540
|
+
return { ok: false, error: "Sidecar URL is required." };
|
|
541
|
+
}
|
|
542
|
+
let parsed;
|
|
543
|
+
try {
|
|
544
|
+
parsed = new URL(url);
|
|
545
|
+
} catch {
|
|
546
|
+
return { ok: false, error: `Sidecar URL is not a valid URL: ${url}` };
|
|
547
|
+
}
|
|
548
|
+
if (parsed.protocol !== "https:") {
|
|
549
|
+
return {
|
|
550
|
+
ok: false,
|
|
551
|
+
error: `Sidecar URL must be https: \u2014 the EVAL_TOKEN is sent as a bearer header and must never travel in cleartext. Got ${parsed.protocol}`
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
const host = parsed.hostname.toLowerCase();
|
|
555
|
+
if (isBlockedHost(host)) {
|
|
556
|
+
return {
|
|
557
|
+
ok: false,
|
|
558
|
+
error: "Sidecar URL must not point at a loopback, private, or link-local address. Use the deployed *.workers.dev URL."
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
if (allowedHosts.length > 0 && !hostMatchesAllowlist(host, allowedHosts)) {
|
|
562
|
+
return {
|
|
563
|
+
ok: false,
|
|
564
|
+
error: `Sidecar host "${host}" is not in this plugin's outbound allowlist (${allowedHosts.join(", ")}). The emdash sandbox would reject the fetch with "Host not allowed" \u2014 update evaluatorHost in astro.config and rebuild, or use that host.`
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
return { ok: true, url };
|
|
487
568
|
}
|
|
569
|
+
function isBlockedHost(host) {
|
|
570
|
+
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
if (host.startsWith("[")) {
|
|
574
|
+
return isBlockedIpv6(host);
|
|
575
|
+
}
|
|
576
|
+
return isBlockedIpv4(host);
|
|
577
|
+
}
|
|
578
|
+
function isBlockedIpv4(host) {
|
|
579
|
+
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
580
|
+
if (match === null) {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
const first = Number(match[1]);
|
|
584
|
+
const second = Number(match[2]);
|
|
585
|
+
if (first === 0) return true;
|
|
586
|
+
if (first === 127) return true;
|
|
587
|
+
if (first === 10) return true;
|
|
588
|
+
if (first === 172 && second >= 16 && second <= 31) return true;
|
|
589
|
+
if (first === 192 && second === 168) return true;
|
|
590
|
+
if (first === 169 && second === 254) return true;
|
|
591
|
+
if (first === 100 && second >= 64 && second <= 127) return true;
|
|
592
|
+
if (first >= 224) return true;
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
function isBlockedIpv6(host) {
|
|
596
|
+
const inner = host.slice(1, -1).toLowerCase();
|
|
597
|
+
if (inner === "::" || inner === "::1") {
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
if (inner.startsWith("::ffff:") || inner.startsWith("::7f")) {
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
const firstHextet = Number.parseInt(inner.split(":")[0] ?? "", 16);
|
|
604
|
+
if (Number.isNaN(firstHextet)) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
if ((firstHextet & 65024) === 64512) {
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
if ((firstHextet & 65472) === 65152) {
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
function hostMatchesAllowlist(host, allowedHosts) {
|
|
616
|
+
return allowedHosts.some((allowed) => {
|
|
617
|
+
const entry = allowed.trim().toLowerCase();
|
|
618
|
+
if (entry.length === 0) {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
if (entry.startsWith("*.")) {
|
|
622
|
+
const suffix = entry.slice(1);
|
|
623
|
+
return host.endsWith(suffix) && host.length > suffix.length;
|
|
624
|
+
}
|
|
625
|
+
return host === entry;
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/evaluator-sandbox.ts
|
|
488
630
|
async function scoreSite(_documents) {
|
|
489
631
|
return {
|
|
490
632
|
overall_score: 0,
|
|
@@ -496,159 +638,8 @@ async function scoreSite(_documents) {
|
|
|
496
638
|
blockers: []
|
|
497
639
|
};
|
|
498
640
|
}
|
|
499
|
-
function errorFindings(findings) {
|
|
500
|
-
return findings.filter((finding) => finding.severity === "error");
|
|
501
|
-
}
|
|
502
|
-
function findingsByRoute(findings) {
|
|
503
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
504
|
-
for (const finding of findings) {
|
|
505
|
-
const bucket = grouped.get(finding.path);
|
|
506
|
-
if (bucket) {
|
|
507
|
-
bucket.push(finding);
|
|
508
|
-
} else {
|
|
509
|
-
grouped.set(finding.path, [finding]);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
return grouped;
|
|
513
|
-
}
|
|
514
641
|
|
|
515
|
-
// src/
|
|
516
|
-
var INDEXNOW_ENDPOINT = "https://api.indexnow.org/indexnow";
|
|
517
|
-
async function submitIndexNow(config, urls) {
|
|
518
|
-
const host = hostOf(config.siteUrl);
|
|
519
|
-
if (host === null) {
|
|
520
|
-
return failure("siteUrl is not a valid URL", []);
|
|
521
|
-
}
|
|
522
|
-
const keyLocation = config.keyLocation ?? defaultKeyLocation(config);
|
|
523
|
-
const { submitted, rejected } = partitionByHost(urls, host);
|
|
524
|
-
if (submitted.length === 0) {
|
|
525
|
-
return failure("no submittable URLs after host check", rejected);
|
|
526
|
-
}
|
|
527
|
-
const payload = {
|
|
528
|
-
host,
|
|
529
|
-
key: config.key,
|
|
530
|
-
keyLocation,
|
|
531
|
-
urlList: submitted
|
|
532
|
-
};
|
|
533
|
-
const response = await fetch(INDEXNOW_ENDPOINT, {
|
|
534
|
-
method: "POST",
|
|
535
|
-
headers: { "content-type": "application/json" },
|
|
536
|
-
body: JSON.stringify(payload)
|
|
537
|
-
});
|
|
538
|
-
return {
|
|
539
|
-
ok: response.ok,
|
|
540
|
-
status: response.status,
|
|
541
|
-
endpoint: INDEXNOW_ENDPOINT,
|
|
542
|
-
submitted,
|
|
543
|
-
rejected
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
function hostOf(rawUrl) {
|
|
547
|
-
try {
|
|
548
|
-
return new URL(rawUrl).host;
|
|
549
|
-
} catch {
|
|
550
|
-
return null;
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
function defaultKeyLocation(config) {
|
|
554
|
-
const siteUrl = config.siteUrl.endsWith("/") ? config.siteUrl.slice(0, -1) : config.siteUrl;
|
|
555
|
-
return `${siteUrl}/${config.key}.txt`;
|
|
556
|
-
}
|
|
557
|
-
function partitionByHost(urls, expectedHost) {
|
|
558
|
-
const submitted = [];
|
|
559
|
-
const rejected = [];
|
|
560
|
-
for (const candidate of urls) {
|
|
561
|
-
const host = hostOf(candidate);
|
|
562
|
-
if (host === expectedHost) {
|
|
563
|
-
submitted.push(candidate);
|
|
564
|
-
} else {
|
|
565
|
-
rejected.push(candidate);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
return { submitted, rejected };
|
|
569
|
-
}
|
|
570
|
-
function failure(reason, rejected) {
|
|
571
|
-
return {
|
|
572
|
-
ok: false,
|
|
573
|
-
status: 0,
|
|
574
|
-
endpoint: INDEXNOW_ENDPOINT,
|
|
575
|
-
submitted: [],
|
|
576
|
-
rejected,
|
|
577
|
-
reason
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// src/mcp.ts
|
|
582
|
-
var checkTool = {
|
|
583
|
-
name: "aexeo.check",
|
|
584
|
-
description: "Evaluate a list of EmdashDocument values against the Aexeo rule engine and return the stable Finding contract. Mirrors the CLI's `aexeo-cli check` surface so agents, the admin UI, and the CI gate all see the same rule ids.",
|
|
585
|
-
inputSchema: {
|
|
586
|
-
type: "object",
|
|
587
|
-
properties: {
|
|
588
|
-
documents: {
|
|
589
|
-
type: "array",
|
|
590
|
-
description: "Array of EmdashDocument values to evaluate. Each entry carries route, title, optional description/canonical/lang, alternates, meta map, schema, and Portable Text body."
|
|
591
|
-
},
|
|
592
|
-
configJson: {
|
|
593
|
-
type: "string",
|
|
594
|
-
description: "Optional JSON-serialized Aexeo config. When absent the evaluator uses Config::default(), matching a cold CLI run."
|
|
595
|
-
}
|
|
596
|
-
},
|
|
597
|
-
required: ["documents"]
|
|
598
|
-
},
|
|
599
|
-
handler: async ({ documents, configJson }) => {
|
|
600
|
-
const options = configJson === void 0 ? {} : { configJson };
|
|
601
|
-
const findings = await evaluate(documents, options);
|
|
602
|
-
const errors = errorFindings(findings);
|
|
603
|
-
const grouped = findingsByRoute(findings);
|
|
604
|
-
const byRoute = {};
|
|
605
|
-
for (const [route, items] of grouped.entries()) {
|
|
606
|
-
byRoute[route] = items;
|
|
607
|
-
}
|
|
608
|
-
return {
|
|
609
|
-
findings,
|
|
610
|
-
errors,
|
|
611
|
-
byRoute,
|
|
612
|
-
totals: {
|
|
613
|
-
findings: findings.length,
|
|
614
|
-
errors: errors.length,
|
|
615
|
-
warnings: findings.length - errors.length
|
|
616
|
-
}
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
};
|
|
620
|
-
var indexNowSubmitTool = {
|
|
621
|
-
name: "aexeo.indexnow.submit",
|
|
622
|
-
description: "Submit a list of URLs to the IndexNow freshness endpoint. Mirrors the aexeo-cli `indexnow submit` contract: the IndexNow protocol requires every submitted URL to belong to the configured site host. URLs that do not match the host are returned in `rejected` and never sent over the wire.",
|
|
623
|
-
inputSchema: {
|
|
624
|
-
type: "object",
|
|
625
|
-
properties: {
|
|
626
|
-
config: {
|
|
627
|
-
type: "object",
|
|
628
|
-
description: "IndexNow configuration: siteUrl, key, and optional keyLocation override.",
|
|
629
|
-
properties: {
|
|
630
|
-
siteUrl: { type: "string" },
|
|
631
|
-
key: { type: "string" },
|
|
632
|
-
keyLocation: { type: "string" }
|
|
633
|
-
},
|
|
634
|
-
required: ["siteUrl", "key"]
|
|
635
|
-
},
|
|
636
|
-
urls: {
|
|
637
|
-
type: "array",
|
|
638
|
-
items: { type: "string" },
|
|
639
|
-
description: "Absolute URLs to submit. Must share siteUrl's host."
|
|
640
|
-
}
|
|
641
|
-
},
|
|
642
|
-
required: ["config", "urls"]
|
|
643
|
-
},
|
|
644
|
-
handler: async ({ config, urls }) => submitIndexNow(config, urls)
|
|
645
|
-
};
|
|
646
|
-
var tools = [checkTool, indexNowSubmitTool];
|
|
647
|
-
|
|
648
|
-
// src/sandbox-entry.ts
|
|
649
|
-
function definePlugin(plugin) {
|
|
650
|
-
return plugin;
|
|
651
|
-
}
|
|
642
|
+
// src/findings-view.ts
|
|
652
643
|
function normalizeInteraction(input) {
|
|
653
644
|
if (!input || typeof input !== "object") {
|
|
654
645
|
return { type: "page_load", page: "/findings" };
|
|
@@ -678,107 +669,31 @@ function normalizeInteraction(input) {
|
|
|
678
669
|
}
|
|
679
670
|
return { type: "page_load", page: "/findings" };
|
|
680
671
|
}
|
|
681
|
-
async function
|
|
682
|
-
const body = normalizeInteraction(input?.input);
|
|
683
|
-
ctx.log?.info?.(
|
|
684
|
-
`aexeo route: type=${body.type} page=${body.type === "page_load" ? body.page : ""}`
|
|
685
|
-
);
|
|
686
|
-
const dispatch = { body, kv: ctx.kv, ctx };
|
|
687
|
-
if (body.type === "page_load") {
|
|
688
|
-
return handlePageLoad(dispatch, body.page);
|
|
689
|
-
}
|
|
690
|
-
if (body.type === "block_action") {
|
|
691
|
-
return handleBlockAction(
|
|
692
|
-
dispatch,
|
|
693
|
-
body.action_id,
|
|
694
|
-
body.value
|
|
695
|
-
);
|
|
696
|
-
}
|
|
697
|
-
if (body.type === "form_submit") {
|
|
698
|
-
return handleFormSubmit(
|
|
699
|
-
dispatch,
|
|
700
|
-
body.action_id,
|
|
701
|
-
body.values
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
return handlePageLoad(dispatch, "findings");
|
|
705
|
-
}
|
|
706
|
-
async function handleFormSubmit(ctx, actionId, values) {
|
|
707
|
-
if (actionId === "save_setup") {
|
|
708
|
-
return handleSetupSubmit(ctx, values);
|
|
709
|
-
}
|
|
710
|
-
if (actionId === "view_document") {
|
|
711
|
-
const picked = values["route_picker"];
|
|
712
|
-
if (typeof picked === "string" && picked.length > 0) {
|
|
713
|
-
return renderDocumentPanel(ctx, picked);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
return handlePageLoad(ctx, "findings");
|
|
717
|
-
}
|
|
718
|
-
async function handlePageLoad(ctx, page) {
|
|
719
|
-
const normalized = page.startsWith("/") ? page.slice(1) : page;
|
|
720
|
-
if (normalized === "findings") {
|
|
721
|
-
return renderFindingsPage(ctx);
|
|
722
|
-
}
|
|
723
|
-
if (normalized === "widget:aexeo-score") {
|
|
724
|
-
return renderScoreWidget(ctx);
|
|
725
|
-
}
|
|
726
|
-
if (normalized === "document") {
|
|
727
|
-
return renderDocumentPanel(ctx);
|
|
728
|
-
}
|
|
729
|
-
if (normalized === "setup") {
|
|
730
|
-
return renderSetupPage(ctx);
|
|
731
|
-
}
|
|
732
|
-
return notFound(page);
|
|
733
|
-
}
|
|
734
|
-
async function handleBlockAction(ctx, actionId, value) {
|
|
735
|
-
if (actionId === "view_document" && typeof value === "string") {
|
|
736
|
-
return renderDocumentPanel(ctx, value);
|
|
737
|
-
}
|
|
738
|
-
if (actionId === "refresh_findings") {
|
|
739
|
-
return handleRefresh(ctx);
|
|
740
|
-
}
|
|
741
|
-
if (actionId.startsWith("filter:")) {
|
|
742
|
-
return renderFindingsPage(ctx);
|
|
743
|
-
}
|
|
744
|
-
return notFound(actionId);
|
|
745
|
-
}
|
|
746
|
-
async function handleRefresh(ctx) {
|
|
747
|
-
const sandboxEvaluator = async (documents) => {
|
|
748
|
-
const runtime = await readSidecarConfig(ctx.kv);
|
|
749
|
-
if (runtime === null) {
|
|
750
|
-
return {
|
|
751
|
-
ok: false,
|
|
752
|
-
reason: "config_missing",
|
|
753
|
-
detail: "sidecar not configured \u2014 open the Aexeo Setup page and enter your evaluator URL and token"
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
return evaluateViaSidecar(
|
|
757
|
-
ctx.ctx.http,
|
|
758
|
-
{ url: runtime.url, authToken: runtime.token },
|
|
759
|
-
documents
|
|
760
|
-
);
|
|
761
|
-
};
|
|
672
|
+
async function handleRefresh(options) {
|
|
762
673
|
let summary;
|
|
763
674
|
try {
|
|
764
|
-
summary = await evaluateAndPersistAll(
|
|
765
|
-
|
|
675
|
+
summary = await evaluateAndPersistAll(options.ctx, {
|
|
676
|
+
collections: options.collections,
|
|
677
|
+
evaluator: options.evaluator,
|
|
678
|
+
// Always forwarded. See RefreshOptions.suppressionFilter.
|
|
679
|
+
suppressionFilter: options.suppressionFilter
|
|
766
680
|
});
|
|
767
681
|
} catch (err) {
|
|
768
682
|
const detail = err instanceof Error ? err.message : String(err);
|
|
683
|
+
const message = `Refresh failed: ${detail}`;
|
|
769
684
|
return {
|
|
770
685
|
blocks: [
|
|
771
686
|
{ type: "header", text: "SEO findings" },
|
|
772
687
|
{
|
|
773
688
|
type: "banner",
|
|
774
|
-
title:
|
|
775
|
-
variant:
|
|
689
|
+
title: message,
|
|
690
|
+
variant: options.tuning.refreshFailureVariant
|
|
776
691
|
}
|
|
777
692
|
],
|
|
778
|
-
toast: { message
|
|
693
|
+
toast: { message, type: "error" }
|
|
779
694
|
};
|
|
780
695
|
}
|
|
781
|
-
const refreshed = await renderFindingsPage(ctx);
|
|
696
|
+
const refreshed = await renderFindingsPage(options.ctx.kv, options.tuning);
|
|
782
697
|
const toastMessage = summary.errors.length === 0 ? `Refreshed ${summary.routesUpdated} routes (${summary.totalFindings} findings across ${summary.documentsScanned} documents)` : `Refresh completed with ${summary.errors.length} errors \u2014 see banner`;
|
|
783
698
|
if (summary.errors.length > 0) {
|
|
784
699
|
refreshed.blocks.unshift({
|
|
@@ -795,10 +710,43 @@ async function handleRefresh(ctx) {
|
|
|
795
710
|
}
|
|
796
711
|
};
|
|
797
712
|
}
|
|
798
|
-
async function
|
|
799
|
-
const
|
|
713
|
+
async function readAllFindings(kv) {
|
|
714
|
+
const entries = await kv.list(
|
|
715
|
+
"findings:"
|
|
716
|
+
);
|
|
717
|
+
const out = [];
|
|
718
|
+
for (const entry of entries) {
|
|
719
|
+
if (entry.value === null) {
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
const route = entry.key.replace(/^findings:/, "");
|
|
723
|
+
for (const finding of entry.value.findings) {
|
|
724
|
+
out.push({ ...finding, document_route: route });
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return out;
|
|
728
|
+
}
|
|
729
|
+
function severityFirst(a, b) {
|
|
730
|
+
const rank = (severity) => severity === "error" ? 0 : 1;
|
|
731
|
+
const diff = rank(a.severity) - rank(b.severity);
|
|
732
|
+
if (diff !== 0) {
|
|
733
|
+
return diff;
|
|
734
|
+
}
|
|
735
|
+
return a.rule_id.localeCompare(b.rule_id);
|
|
736
|
+
}
|
|
737
|
+
function uniqueRoutes(rows) {
|
|
738
|
+
const routes = /* @__PURE__ */ new Set();
|
|
739
|
+
for (const row of rows) {
|
|
740
|
+
routes.add(row.document_route);
|
|
741
|
+
}
|
|
742
|
+
return [...routes].sort();
|
|
743
|
+
}
|
|
744
|
+
async function renderFindingsPage(kv, tuning) {
|
|
745
|
+
const findings = await readAllFindings(kv);
|
|
800
746
|
const errors = findings.filter((finding) => finding.severity === "error");
|
|
801
|
-
const warnings = findings.filter(
|
|
747
|
+
const warnings = findings.filter(
|
|
748
|
+
(finding) => finding.severity === "warning"
|
|
749
|
+
);
|
|
802
750
|
const sorted = [...findings].sort(severityFirst);
|
|
803
751
|
const routes = uniqueRoutes(findings);
|
|
804
752
|
const blocks = [
|
|
@@ -812,8 +760,8 @@ async function renderFindingsPage(ctx) {
|
|
|
812
760
|
type: "actions",
|
|
813
761
|
elements: [
|
|
814
762
|
// Refresh evaluates every configured entry. Pressing it lists
|
|
815
|
-
// content via the live in-request bridge,
|
|
816
|
-
//
|
|
763
|
+
// content via the live in-request bridge, evaluates it, and
|
|
764
|
+
// writes findings back to KV (with suppressions applied).
|
|
817
765
|
{
|
|
818
766
|
type: "button",
|
|
819
767
|
label: "Refresh",
|
|
@@ -831,7 +779,7 @@ async function renderFindingsPage(ctx) {
|
|
|
831
779
|
},
|
|
832
780
|
sorted.length === 0 ? {
|
|
833
781
|
type: "context",
|
|
834
|
-
text:
|
|
782
|
+
text: tuning.emptyFindingsText
|
|
835
783
|
} : findingsTable(sorted)
|
|
836
784
|
];
|
|
837
785
|
if (routes.length > 0) {
|
|
@@ -850,37 +798,6 @@ async function renderFindingsPage(ctx) {
|
|
|
850
798
|
}
|
|
851
799
|
return { blocks };
|
|
852
800
|
}
|
|
853
|
-
async function readAllFindings(kv) {
|
|
854
|
-
const entries = await kv.list(
|
|
855
|
-
"findings:"
|
|
856
|
-
);
|
|
857
|
-
const out = [];
|
|
858
|
-
for (const entry of entries) {
|
|
859
|
-
if (entry.value === null) {
|
|
860
|
-
continue;
|
|
861
|
-
}
|
|
862
|
-
const route = entry.key.replace(/^findings:/, "");
|
|
863
|
-
for (const finding of entry.value.findings) {
|
|
864
|
-
out.push({ ...finding, document_route: route });
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
return out;
|
|
868
|
-
}
|
|
869
|
-
function uniqueRoutes(rows) {
|
|
870
|
-
const routes = /* @__PURE__ */ new Set();
|
|
871
|
-
for (const row of rows) {
|
|
872
|
-
routes.add(row.document_route);
|
|
873
|
-
}
|
|
874
|
-
return [...routes].sort();
|
|
875
|
-
}
|
|
876
|
-
function severityFirst(a, b) {
|
|
877
|
-
const rank = (severity) => severity === "error" ? 0 : 1;
|
|
878
|
-
const diff = rank(a.severity) - rank(b.severity);
|
|
879
|
-
if (diff !== 0) {
|
|
880
|
-
return diff;
|
|
881
|
-
}
|
|
882
|
-
return a.rule_id.localeCompare(b.rule_id);
|
|
883
|
-
}
|
|
884
801
|
function findingsTable(rows) {
|
|
885
802
|
return {
|
|
886
803
|
type: "table",
|
|
@@ -898,40 +815,31 @@ function findingsTable(rows) {
|
|
|
898
815
|
}))
|
|
899
816
|
};
|
|
900
817
|
}
|
|
901
|
-
async function renderScoreWidget(
|
|
902
|
-
const documents = await readAllDocuments(
|
|
818
|
+
async function renderScoreWidget(kv, tuning) {
|
|
819
|
+
const documents = await readAllDocuments(kv);
|
|
903
820
|
if (documents.length === 0) {
|
|
904
821
|
return {
|
|
905
822
|
blocks: [
|
|
906
823
|
{ type: "header", text: "SEO score" },
|
|
907
824
|
{
|
|
908
825
|
type: "context",
|
|
909
|
-
text:
|
|
826
|
+
text: tuning.emptyScoreText
|
|
910
827
|
}
|
|
911
828
|
]
|
|
912
829
|
};
|
|
913
830
|
}
|
|
914
|
-
const score = await
|
|
831
|
+
const score = await tuning.score(documents, kv);
|
|
915
832
|
const blocks = [
|
|
916
833
|
{
|
|
917
834
|
type: "stats",
|
|
918
835
|
items: [
|
|
836
|
+
{ label: "Overall", value: `${score.overall_score}` },
|
|
837
|
+
{ label: "Citation", value: `${score.citation_readiness_score}` },
|
|
919
838
|
{
|
|
920
|
-
label:
|
|
921
|
-
value: `${score.overall_score}`
|
|
922
|
-
},
|
|
923
|
-
{
|
|
924
|
-
label: "Citation",
|
|
925
|
-
value: `${score.citation_readiness_score}`
|
|
926
|
-
},
|
|
927
|
-
{
|
|
928
|
-
label: "Truth",
|
|
839
|
+
label: tuning.truthLabel(score),
|
|
929
840
|
value: `${score.truth_consistency_score}`
|
|
930
841
|
},
|
|
931
|
-
{
|
|
932
|
-
label: "Answers",
|
|
933
|
-
value: `${score.answer_pack_score}`
|
|
934
|
-
}
|
|
842
|
+
{ label: "Answers", value: `${score.answer_pack_score}` }
|
|
935
843
|
]
|
|
936
844
|
}
|
|
937
845
|
];
|
|
@@ -957,7 +865,7 @@ function topBlockersLine(score) {
|
|
|
957
865
|
}
|
|
958
866
|
return `Top blockers: ${top.join(" \u2022 ")}`;
|
|
959
867
|
}
|
|
960
|
-
async function renderDocumentPanel(
|
|
868
|
+
async function renderDocumentPanel(kv, route) {
|
|
961
869
|
if (route === void 0) {
|
|
962
870
|
return {
|
|
963
871
|
blocks: [
|
|
@@ -969,9 +877,11 @@ async function renderDocumentPanel(ctx, route) {
|
|
|
969
877
|
]
|
|
970
878
|
};
|
|
971
879
|
}
|
|
972
|
-
const findings = await readFindings(
|
|
880
|
+
const findings = await readFindings(kv, route);
|
|
973
881
|
const errors = findings.filter((finding) => finding.severity === "error");
|
|
974
|
-
const warnings = findings.filter(
|
|
882
|
+
const warnings = findings.filter(
|
|
883
|
+
(finding) => finding.severity === "warning"
|
|
884
|
+
);
|
|
975
885
|
const sorted = [...findings].sort(severityFirst);
|
|
976
886
|
return {
|
|
977
887
|
blocks: [
|
|
@@ -1004,13 +914,185 @@ function documentFindingsTable(findings) {
|
|
|
1004
914
|
}))
|
|
1005
915
|
};
|
|
1006
916
|
}
|
|
917
|
+
function notFound(page) {
|
|
918
|
+
return {
|
|
919
|
+
blocks: [
|
|
920
|
+
{ type: "header", text: "Not found" },
|
|
921
|
+
{ type: "context", text: `unknown page: ${page}` }
|
|
922
|
+
]
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// src/suppressions.ts
|
|
927
|
+
var NOOP_FILTER = {
|
|
928
|
+
apply: (_context, findings) => [...findings]
|
|
929
|
+
};
|
|
930
|
+
function compileSuppressions(rules) {
|
|
931
|
+
if (rules === void 0 || rules.length === 0) {
|
|
932
|
+
return NOOP_FILTER;
|
|
933
|
+
}
|
|
934
|
+
rules.forEach((rule, index) => {
|
|
935
|
+
const hasRoute = rule.routePattern !== void 0 && rule.routePattern.length > 0;
|
|
936
|
+
const hasRuleIds = rule.ruleIds !== void 0 && rule.ruleIds.length > 0;
|
|
937
|
+
const hasCollections = rule.collections !== void 0 && rule.collections.length > 0;
|
|
938
|
+
const hasStatuses = rule.statuses !== void 0 && rule.statuses.length > 0;
|
|
939
|
+
if (!hasRoute && !hasRuleIds && !hasCollections && !hasStatuses) {
|
|
940
|
+
throw new Error(
|
|
941
|
+
`aexeoPlugin: suppressions[${index}] is empty \u2014 set at least one of routePattern, ruleIds, collections, statuses`
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
const compiled = rules.map((rule) => ({
|
|
946
|
+
matchRoute: rule.routePattern === void 0 ? matchAlways : globToMatcher(rule.routePattern),
|
|
947
|
+
ruleIds: rule.ruleIds === void 0 || rule.ruleIds.length === 0 ? null : new Set(rule.ruleIds),
|
|
948
|
+
collections: rule.collections === void 0 || rule.collections.length === 0 ? null : new Set(rule.collections),
|
|
949
|
+
statuses: rule.statuses === void 0 || rule.statuses.length === 0 ? null : new Set(rule.statuses)
|
|
950
|
+
}));
|
|
951
|
+
return {
|
|
952
|
+
apply(context, findings) {
|
|
953
|
+
return findings.filter(
|
|
954
|
+
(finding) => !compiled.some((rule) => {
|
|
955
|
+
if (!rule.matchRoute(context.route)) return false;
|
|
956
|
+
if (rule.ruleIds !== null && !rule.ruleIds.has(finding.rule_id)) {
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
if (rule.collections !== null) {
|
|
960
|
+
if (context.collection === void 0 || !rule.collections.has(context.collection)) {
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if (rule.statuses !== null) {
|
|
965
|
+
if (context.status === void 0 || !rule.statuses.has(context.status)) {
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return true;
|
|
970
|
+
})
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
var matchAlways = () => true;
|
|
976
|
+
function globToMatcher(pattern) {
|
|
977
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
978
|
+
const DOUBLE_STAR_TOKEN = "";
|
|
979
|
+
const body = escaped.replace(/\*\*/g, DOUBLE_STAR_TOKEN).replace(/\*/g, "[^/]*").replace(new RegExp(DOUBLE_STAR_TOKEN, "g"), ".*").replace(/\?/g, "[^/]");
|
|
980
|
+
const re = new RegExp(`^${body}$`);
|
|
981
|
+
return (route) => re.test(route);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// src/sandbox-entry.ts
|
|
985
|
+
function definePlugin(plugin) {
|
|
986
|
+
return plugin;
|
|
987
|
+
}
|
|
988
|
+
var sandboxSuppressionFilter = compileSuppressions(
|
|
989
|
+
void 0
|
|
990
|
+
);
|
|
991
|
+
var adminViewTuning = {
|
|
992
|
+
emptyFindingsText: "Once a document publishes, its rule findings list here.",
|
|
993
|
+
emptyScoreText: "No documents saved yet \u2014 score appears after the first emdash save.",
|
|
994
|
+
refreshFailureVariant: "alert",
|
|
995
|
+
score: async (documents) => scoreSite(documents),
|
|
996
|
+
truthLabel: () => "Truth"
|
|
997
|
+
};
|
|
998
|
+
async function handleAdminRoute(input, ctx) {
|
|
999
|
+
const body = normalizeInteraction(input?.input);
|
|
1000
|
+
ctx.log?.info?.(
|
|
1001
|
+
`aexeo route: type=${body.type} page=${body.type === "page_load" ? body.page : ""}`
|
|
1002
|
+
);
|
|
1003
|
+
const dispatch = { body, kv: ctx.kv, ctx };
|
|
1004
|
+
if (body.type === "page_load") {
|
|
1005
|
+
return handlePageLoad(dispatch, body.page);
|
|
1006
|
+
}
|
|
1007
|
+
if (body.type === "block_action") {
|
|
1008
|
+
return handleBlockAction(
|
|
1009
|
+
dispatch,
|
|
1010
|
+
body.action_id,
|
|
1011
|
+
body.value
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
if (body.type === "form_submit") {
|
|
1015
|
+
return handleFormSubmit(
|
|
1016
|
+
dispatch,
|
|
1017
|
+
body.action_id,
|
|
1018
|
+
body.values
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
return handlePageLoad(dispatch, "findings");
|
|
1022
|
+
}
|
|
1023
|
+
async function handleFormSubmit(ctx, actionId, values) {
|
|
1024
|
+
if (actionId === "save_setup") {
|
|
1025
|
+
return handleSetupSubmit(ctx, values);
|
|
1026
|
+
}
|
|
1027
|
+
if (actionId === "view_document") {
|
|
1028
|
+
const picked = values["route_picker"];
|
|
1029
|
+
if (typeof picked === "string" && picked.length > 0) {
|
|
1030
|
+
return renderDocumentPanel(ctx.kv, picked);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return handlePageLoad(ctx, "findings");
|
|
1034
|
+
}
|
|
1035
|
+
async function handlePageLoad(ctx, page) {
|
|
1036
|
+
const normalized = page.startsWith("/") ? page.slice(1) : page;
|
|
1037
|
+
if (normalized === "findings") {
|
|
1038
|
+
return renderFindingsPage(ctx.kv, adminViewTuning);
|
|
1039
|
+
}
|
|
1040
|
+
if (normalized === "widget:aexeo-score") {
|
|
1041
|
+
return renderScoreWidget(ctx.kv, adminViewTuning);
|
|
1042
|
+
}
|
|
1043
|
+
if (normalized === "document") {
|
|
1044
|
+
return renderDocumentPanel(ctx.kv);
|
|
1045
|
+
}
|
|
1046
|
+
if (normalized === "setup") {
|
|
1047
|
+
return renderSetupPage(ctx);
|
|
1048
|
+
}
|
|
1049
|
+
return notFound(page);
|
|
1050
|
+
}
|
|
1051
|
+
async function handleBlockAction(ctx, actionId, value) {
|
|
1052
|
+
if (actionId === "view_document" && typeof value === "string") {
|
|
1053
|
+
return renderDocumentPanel(ctx.kv, value);
|
|
1054
|
+
}
|
|
1055
|
+
if (actionId === "refresh_findings") {
|
|
1056
|
+
return handleRefresh2(ctx);
|
|
1057
|
+
}
|
|
1058
|
+
if (actionId.startsWith("filter:")) {
|
|
1059
|
+
return renderFindingsPage(ctx.kv, adminViewTuning);
|
|
1060
|
+
}
|
|
1061
|
+
return notFound(actionId);
|
|
1062
|
+
}
|
|
1063
|
+
async function handleRefresh2(ctx) {
|
|
1064
|
+
const sandboxEvaluator = async (documents) => {
|
|
1065
|
+
const runtime = await readSidecarConfig(ctx.ctx);
|
|
1066
|
+
if (runtime === null) {
|
|
1067
|
+
return {
|
|
1068
|
+
ok: false,
|
|
1069
|
+
reason: "config_missing",
|
|
1070
|
+
detail: "sidecar not configured \u2014 open the Aexeo Setup page and enter your evaluator URL and token"
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
return evaluateViaSidecar(
|
|
1074
|
+
ctx.ctx.http,
|
|
1075
|
+
{ url: runtime.url, authToken: runtime.token },
|
|
1076
|
+
documents
|
|
1077
|
+
);
|
|
1078
|
+
};
|
|
1079
|
+
return handleRefresh({
|
|
1080
|
+
ctx: ctx.ctx,
|
|
1081
|
+
// The sandboxed descriptor takes no collection knobs, so the sweep
|
|
1082
|
+
// always covers the default set.
|
|
1083
|
+
collections: DEFAULT_COLLECTIONS,
|
|
1084
|
+
evaluator: sandboxEvaluator,
|
|
1085
|
+
suppressionFilter: sandboxSuppressionFilter,
|
|
1086
|
+
tuning: adminViewTuning
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1007
1089
|
async function renderSetupPage(ctx, options = {}) {
|
|
1008
|
-
const existing = await readSidecarConfig(ctx.
|
|
1090
|
+
const existing = await readSidecarConfig(ctx.ctx);
|
|
1009
1091
|
const blocks = [
|
|
1010
1092
|
{ type: "header", text: "Aexeo setup" },
|
|
1011
1093
|
{
|
|
1012
1094
|
type: "context",
|
|
1013
|
-
text: "Paste the URL and auth token of your deployed aexeo-crawl-worker.
|
|
1095
|
+
text: "Paste the URL and auth token of your deployed aexeo-crawl-worker. The token is stored encrypted by the emdash host; both values are read at runtime, so no rebuild is required after a change. Rotate the token here whenever you redeploy the sidecar with a new secret."
|
|
1014
1096
|
}
|
|
1015
1097
|
];
|
|
1016
1098
|
if (options.bannerError !== void 0) {
|
|
@@ -1058,39 +1140,23 @@ async function renderSetupPage(ctx, options = {}) {
|
|
|
1058
1140
|
);
|
|
1059
1141
|
return { blocks };
|
|
1060
1142
|
}
|
|
1143
|
+
function runtimeAllowedHosts() {
|
|
1144
|
+
const env = typeof process === "undefined" ? void 0 : process.env;
|
|
1145
|
+
return buildAllowedHosts(env?.["AEXEO_EVALUATOR_HOST"] ?? null);
|
|
1146
|
+
}
|
|
1061
1147
|
async function handleSetupSubmit(ctx, values) {
|
|
1062
1148
|
const rawUrl = values["evaluator_url"];
|
|
1063
1149
|
const rawToken = values["eval_token"];
|
|
1064
1150
|
const url = typeof rawUrl === "string" ? rawUrl.trim() : "";
|
|
1065
1151
|
const token = typeof rawToken === "string" ? rawToken.trim() : "";
|
|
1066
|
-
|
|
1152
|
+
const validated = validateSidecarUrl(url, runtimeAllowedHosts());
|
|
1153
|
+
if (!validated.ok) {
|
|
1067
1154
|
return renderSetupPage(ctx, {
|
|
1068
|
-
bannerError:
|
|
1155
|
+
bannerError: validated.error,
|
|
1069
1156
|
initialUrl: url
|
|
1070
1157
|
});
|
|
1071
1158
|
}
|
|
1072
|
-
|
|
1073
|
-
try {
|
|
1074
|
-
parsed = new URL(url);
|
|
1075
|
-
} catch {
|
|
1076
|
-
return renderSetupPage(ctx, {
|
|
1077
|
-
bannerError: `Sidecar URL is not a valid URL: ${url}`,
|
|
1078
|
-
initialUrl: url
|
|
1079
|
-
});
|
|
1080
|
-
}
|
|
1081
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
1082
|
-
return renderSetupPage(ctx, {
|
|
1083
|
-
bannerError: `Sidecar URL must be http(s): got ${parsed.protocol}`,
|
|
1084
|
-
initialUrl: url
|
|
1085
|
-
});
|
|
1086
|
-
}
|
|
1087
|
-
if (parsed.hostname === "localhost" || parsed.hostname.endsWith(".localhost") || /^127\./.test(parsed.hostname)) {
|
|
1088
|
-
return renderSetupPage(ctx, {
|
|
1089
|
-
bannerError: "emdash's sandbox blocks fetches to localhost (anti-SSRF). Use the deployed *.workers.dev URL.",
|
|
1090
|
-
initialUrl: url
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
const existing = await readSidecarConfig(ctx.kv);
|
|
1159
|
+
const existing = await readSidecarConfig(ctx.ctx);
|
|
1094
1160
|
let resolvedToken;
|
|
1095
1161
|
if (token.length > 0) {
|
|
1096
1162
|
resolvedToken = token;
|
|
@@ -1099,34 +1165,25 @@ async function handleSetupSubmit(ctx, values) {
|
|
|
1099
1165
|
} else {
|
|
1100
1166
|
return renderSetupPage(ctx, {
|
|
1101
1167
|
bannerError: "EVAL_TOKEN is required on first setup.",
|
|
1102
|
-
initialUrl: url
|
|
1168
|
+
initialUrl: validated.url
|
|
1103
1169
|
});
|
|
1104
1170
|
}
|
|
1105
|
-
const next = {
|
|
1106
|
-
|
|
1171
|
+
const next = {
|
|
1172
|
+
url: validated.url,
|
|
1173
|
+
token: resolvedToken
|
|
1174
|
+
};
|
|
1175
|
+
await writeSidecarConfig(ctx.ctx, next);
|
|
1107
1176
|
return renderSetupPage(ctx, {
|
|
1108
1177
|
bannerSuccess: "Configuration saved. Click Refresh on the findings page to evaluate."
|
|
1109
1178
|
});
|
|
1110
1179
|
}
|
|
1111
|
-
function notFound(page) {
|
|
1112
|
-
return {
|
|
1113
|
-
blocks: [
|
|
1114
|
-
{ type: "header", text: "Not found" },
|
|
1115
|
-
{ type: "context", text: `unknown page: ${page}` }
|
|
1116
|
-
]
|
|
1117
|
-
};
|
|
1118
|
-
}
|
|
1119
1180
|
var sandbox_entry_default = definePlugin({
|
|
1120
1181
|
hooks: {
|
|
1121
1182
|
"content:afterSave": handleAfterSave
|
|
1122
1183
|
},
|
|
1123
1184
|
routes: {
|
|
1124
1185
|
admin: { handler: handleAdminRoute }
|
|
1125
|
-
}
|
|
1126
|
-
// emdash's MCP server picks tools up from this field. The exact
|
|
1127
|
-
// host-side spec for plugin-contributed tools is still being mapped;
|
|
1128
|
-
// the field is harmless if unrecognized.
|
|
1129
|
-
mcpTools: tools
|
|
1186
|
+
}
|
|
1130
1187
|
});
|
|
1131
1188
|
export {
|
|
1132
1189
|
sandbox_entry_default as default
|