@aeptus/aexeo-emdash 0.8.18 → 0.9.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/CHANGELOG.md +196 -4
- package/INSTALL.md +126 -8
- package/LICENSE +0 -0
- package/README.md +12 -5
- package/dist/adapter.d.ts +30 -2
- package/dist/adapter.d.ts.map +1 -1
- 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 +1 -1
- package/dist/admin.js +2 -2
- package/dist/configured.d.ts +6 -0
- package/dist/configured.d.ts.map +1 -1
- package/dist/configured.js +817 -334
- package/dist/content-evaluator.d.ts +14 -0
- package/dist/content-evaluator.d.ts.map +1 -0
- package/dist/content.d.ts +51 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +365 -0
- package/dist/data-route.d.ts +3 -0
- package/dist/data-route.d.ts.map +1 -1
- package/dist/evaluator-sandbox.d.ts +1 -1
- package/dist/evaluator-sandbox.d.ts.map +1 -1
- package/dist/evaluator.d.ts +1 -1
- package/dist/evaluator.d.ts.map +1 -1
- package/dist/facts-route.d.ts +0 -0
- package/dist/facts-route.d.ts.map +0 -0
- package/dist/findings-view.d.ts +141 -0
- package/dist/findings-view.d.ts.map +1 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +111 -5
- package/dist/inventory.d.ts +51 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/plugin.d.ts +51 -4
- package/dist/plugin.d.ts.map +1 -1
- package/dist/presence-check.d.ts +1 -0
- package/dist/presence-check.d.ts.map +1 -1
- package/dist/presence-route.d.ts +0 -0
- package/dist/presence-route.d.ts.map +0 -0
- package/dist/sandbox-entry.d.ts +4 -23
- package/dist/sandbox-entry.d.ts.map +1 -1
- package/dist/sandbox-entry.js +738 -396
- 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 +7 -1
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/suppressions.d.ts +0 -0
- package/dist/suppressions.d.ts.map +0 -0
- package/dist/types.d.ts +11 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/wasm-init.d.ts +4 -0
- package/dist/wasm-init.d.ts.map +1 -1
- package/package.json +21 -8
- package/wasm/aexeo_emdash_bridge.d.ts +11 -0
- package/wasm/aexeo_emdash_bridge.js +1 -1
- package/wasm/aexeo_emdash_bridge_bg.d.ts +11 -0
- package/wasm/aexeo_emdash_bridge_bg.js +68 -0
- package/wasm/aexeo_emdash_bridge_bg.wasm +0 -0
- package/wasm/aexeo_emdash_bridge_bg.wasm.d.ts +2 -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
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// src/adapter.ts
|
|
2
|
-
async function adaptContentItemWithContext(content, contentApi, defaultLocale) {
|
|
2
|
+
async function adaptContentItemWithContext(content, contentApi, defaultLocale, blockMappings, contentFields) {
|
|
3
3
|
const publicUrl = content.status === "published" ? await contentApi.getPublicUrl(content.type, content.id) : null;
|
|
4
4
|
const route = publicUrl === null ? deriveRoute(content) : routeFromPublicUrl(publicUrl);
|
|
5
5
|
const adapted = adaptContentItem(content, {
|
|
6
6
|
route,
|
|
7
7
|
publicUrl,
|
|
8
|
-
...defaultLocale === void 0 ? {} : { defaultLocale }
|
|
8
|
+
...defaultLocale === void 0 ? {} : { defaultLocale },
|
|
9
|
+
...blockMappings === void 0 ? {} : { blockMappings },
|
|
10
|
+
...contentFields === void 0 ? {} : { contentFields }
|
|
9
11
|
});
|
|
10
12
|
const translationSet = await contentApi.getTranslations(
|
|
11
13
|
content.type,
|
|
@@ -47,8 +49,11 @@ function adaptContentItem(content, options = {}) {
|
|
|
47
49
|
if (typeof locale === "string" && locale.length > 0) {
|
|
48
50
|
document.lang = locale;
|
|
49
51
|
}
|
|
50
|
-
const body = extractPortableText(content.data);
|
|
51
|
-
if (body !== null)
|
|
52
|
+
const body = options.contentFields === void 0 ? extractPortableText(content.data, options.blockMappings) : extractSelectedContent(content.data, options.contentFields, options.blockMappings);
|
|
53
|
+
if (body !== null) {
|
|
54
|
+
document.body = body.blocks;
|
|
55
|
+
if (body.capabilities !== void 0) document.body_capabilities = body.capabilities;
|
|
56
|
+
}
|
|
52
57
|
return {
|
|
53
58
|
document,
|
|
54
59
|
meta: {
|
|
@@ -57,7 +62,8 @@ function adaptContentItem(content, options = {}) {
|
|
|
57
62
|
slug: content.slug,
|
|
58
63
|
status: content.status,
|
|
59
64
|
title,
|
|
60
|
-
publicUrl: options.publicUrl ?? null
|
|
65
|
+
publicUrl: options.publicUrl ?? null,
|
|
66
|
+
...content.updatedAt === void 0 ? {} : { updatedAt: content.updatedAt }
|
|
61
67
|
}
|
|
62
68
|
};
|
|
63
69
|
}
|
|
@@ -76,24 +82,53 @@ function routeFromPublicUrl(publicUrl) {
|
|
|
76
82
|
function stringOrEmpty(value) {
|
|
77
83
|
return typeof value === "string" ? value : "";
|
|
78
84
|
}
|
|
79
|
-
function
|
|
85
|
+
function extractSelectedContent(data, fields, mappings) {
|
|
86
|
+
if (fields.length === 0 || new Set(fields).size !== fields.length || fields.some((field) => typeof field !== "string" || field.length === 0)) return null;
|
|
87
|
+
const all = ["text", "headings", "paragraphs", "links"];
|
|
88
|
+
let capabilities = all;
|
|
89
|
+
const blocks = [];
|
|
90
|
+
let observed = false;
|
|
91
|
+
for (const field of fields) {
|
|
92
|
+
const value = data[field];
|
|
93
|
+
if (value === void 0 || value === null) continue;
|
|
94
|
+
observed = true;
|
|
95
|
+
const part = extractFirstPortableText({ content: value }, mappings);
|
|
96
|
+
if (part === null) return { blocks, capabilities: [] };
|
|
97
|
+
blocks.push(...part.blocks);
|
|
98
|
+
capabilities = capabilities.filter((capability) => part.capabilities === void 0 || part.capabilities.includes(capability));
|
|
99
|
+
}
|
|
100
|
+
if (!observed) return null;
|
|
101
|
+
return capabilities.length === all.length ? { blocks } : { blocks, capabilities: [...capabilities] };
|
|
102
|
+
}
|
|
103
|
+
function extractPortableText(data, mappings) {
|
|
104
|
+
const result = extractFirstPortableText(data, mappings);
|
|
105
|
+
if (result === null) return null;
|
|
106
|
+
const candidates = Object.entries(data).filter(
|
|
107
|
+
([field, value]) => isPortableTextArray(value) || isEmdashBlocksArray(value, field) || (field === "body" || field === "content") && typeof value === "string" && value.trim().length > 0
|
|
108
|
+
);
|
|
109
|
+
return candidates.length > 1 ? { blocks: result.blocks, capabilities: [] } : result;
|
|
110
|
+
}
|
|
111
|
+
function extractFirstPortableText(data, mappings) {
|
|
112
|
+
let foundEmptyBody = false;
|
|
80
113
|
for (const slug of ["body", "content"]) {
|
|
81
114
|
const value = data[slug];
|
|
82
|
-
if (
|
|
83
|
-
if (
|
|
115
|
+
if (Array.isArray(value) && value.length === 0 || typeof value === "string" && value.trim().length === 0) foundEmptyBody = true;
|
|
116
|
+
if (isPortableTextArray(value)) return { blocks: value };
|
|
117
|
+
if (isEmdashBlocksArray(value, slug)) return mapCustomBlocks(value, mappings);
|
|
84
118
|
if (typeof value === "string" && value.trim().length > 0) {
|
|
85
|
-
return [portableTextBlock(value.trim(), `${slug}-text`)];
|
|
119
|
+
return { blocks: [portableTextBlock(value.trim(), `${slug}-text`)], capabilities: ["text"] };
|
|
86
120
|
}
|
|
87
121
|
}
|
|
88
122
|
const blocks = data["blocks"];
|
|
89
|
-
if (
|
|
90
|
-
if (
|
|
123
|
+
if (Array.isArray(blocks) && blocks.length === 0) foundEmptyBody = true;
|
|
124
|
+
if (isPortableTextArray(blocks)) return { blocks };
|
|
125
|
+
if (isEmdashBlocksArray(blocks, "blocks")) return mapCustomBlocks(blocks, mappings);
|
|
91
126
|
for (const [field, value] of Object.entries(data)) {
|
|
92
127
|
if (field === "body" || field === "content" || field === "blocks") continue;
|
|
93
|
-
if (isPortableTextArray(value)) return value;
|
|
94
|
-
if (isEmdashBlocksArray(value, field)) return
|
|
128
|
+
if (isPortableTextArray(value)) return { blocks: value };
|
|
129
|
+
if (isEmdashBlocksArray(value, field)) return mapCustomBlocks(value, mappings);
|
|
95
130
|
}
|
|
96
|
-
return null;
|
|
131
|
+
return foundEmptyBody ? { blocks: [] } : null;
|
|
97
132
|
}
|
|
98
133
|
function isPortableTextArray(value) {
|
|
99
134
|
return Array.isArray(value) && value.length > 0 && value.every(
|
|
@@ -108,6 +143,52 @@ function isEmdashBlocksArray(value, field) {
|
|
|
108
143
|
(item) => isRecord(item) && typeof item["_type"] === "string" && typeof item["_key"] === "string" && (typeof item["_version"] === "number" || field === "blocks")
|
|
109
144
|
);
|
|
110
145
|
}
|
|
146
|
+
function mapCustomBlocks(blocks, mappings) {
|
|
147
|
+
const unavailable = () => ({ blocks: blocksToPortableText(blocks), capabilities: [] });
|
|
148
|
+
const result = [];
|
|
149
|
+
const mapFields = (record, fields, depth) => {
|
|
150
|
+
if (depth > 8 || fields.length === 0) return false;
|
|
151
|
+
for (const entry of fields) {
|
|
152
|
+
if (!entry || typeof entry.field !== "string" || !entry.field || !["heading", "paragraph", "richText", "link", "repeat"].includes(entry.kind)) return false;
|
|
153
|
+
if (entry.kind === "heading" && entry.level !== void 0 && ![1, 2, 3, 4, 5, 6].includes(entry.level)) return false;
|
|
154
|
+
if (entry.kind === "link" && (typeof entry.hrefField !== "string" || !entry.hrefField)) return false;
|
|
155
|
+
if (entry.kind === "repeat" && (!Array.isArray(entry.fields) || entry.fields.length === 0)) return false;
|
|
156
|
+
const value = record[entry.field];
|
|
157
|
+
if ((value === void 0 || value === null || value === "") && entry.optional) {
|
|
158
|
+
if (entry.kind === "link" && record[entry.hrefField] != null && record[entry.hrefField] !== "") return false;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (entry.kind === "richText") {
|
|
162
|
+
if (!Array.isArray(value) || value.length > 0 && !isPortableTextArray(value)) return false;
|
|
163
|
+
result.push(...value);
|
|
164
|
+
} else if (entry.kind === "heading" || entry.kind === "paragraph") {
|
|
165
|
+
if (typeof value !== "string") return false;
|
|
166
|
+
result.push({ ...portableTextBlock(value, ""), style: entry.kind === "heading" ? `h${entry.level ?? 2}` : "normal" });
|
|
167
|
+
} else if (entry.kind === "link") {
|
|
168
|
+
const href = record[entry.hrefField];
|
|
169
|
+
if (typeof value !== "string" || typeof href !== "string" || !href) return false;
|
|
170
|
+
result.push({
|
|
171
|
+
_type: "block",
|
|
172
|
+
children: [{ _type: "span", text: value, marks: ["link"] }],
|
|
173
|
+
markDefs: [{ _key: "link", _type: "link", href }]
|
|
174
|
+
});
|
|
175
|
+
} else if (entry.kind === "repeat") {
|
|
176
|
+
if (!Array.isArray(value) || !Array.isArray(entry.fields)) return false;
|
|
177
|
+
for (const item of value) {
|
|
178
|
+
if (!isRecord(item) || !mapFields(item, entry.fields, depth + 1)) return false;
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
};
|
|
186
|
+
for (const block of blocks) {
|
|
187
|
+
const fields = mappings?.[String(block["_type"])];
|
|
188
|
+
if (!Array.isArray(fields) || !mapFields(block, fields, 0)) return unavailable();
|
|
189
|
+
}
|
|
190
|
+
return { blocks: result };
|
|
191
|
+
}
|
|
111
192
|
function blocksToPortableText(blocks) {
|
|
112
193
|
const paragraphs = [];
|
|
113
194
|
for (const block of blocks) {
|
|
@@ -191,11 +272,15 @@ function isRecord(value) {
|
|
|
191
272
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
192
273
|
}
|
|
193
274
|
|
|
275
|
+
// src/version.ts
|
|
276
|
+
var PACKAGE_VERSION = "0.9.0";
|
|
277
|
+
|
|
194
278
|
// src/sidecar.ts
|
|
195
|
-
async function evaluateViaSidecar(http, config, documents) {
|
|
279
|
+
async function evaluateViaSidecar(http, config, documents, collections) {
|
|
196
280
|
const body = {
|
|
197
281
|
documents
|
|
198
282
|
};
|
|
283
|
+
if (collections !== void 0) body.collections = collections;
|
|
199
284
|
if (config.configJson !== void 0) {
|
|
200
285
|
body.configJson = config.configJson;
|
|
201
286
|
}
|
|
@@ -228,6 +313,12 @@ async function evaluateViaSidecar(http, config, documents) {
|
|
|
228
313
|
}
|
|
229
314
|
return { ok: false, reason: "server_error", detail };
|
|
230
315
|
}
|
|
316
|
+
const workerVersion = response.headers?.get("x-aexeo-version") ?? "unknown";
|
|
317
|
+
if (workerVersion !== PACKAGE_VERSION) {
|
|
318
|
+
config.log?.warn(
|
|
319
|
+
`Aexeo sidecar is version ${workerVersion} but the plugin is ${PACKAGE_VERSION}; redeploy the Worker so both run the same rules`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
231
322
|
let raw;
|
|
232
323
|
try {
|
|
233
324
|
raw = await response.text();
|
|
@@ -255,11 +346,41 @@ function trimTrailingSlash(url) {
|
|
|
255
346
|
}
|
|
256
347
|
|
|
257
348
|
// src/plugin.ts
|
|
349
|
+
function buildAllowedHosts(evaluatorHost) {
|
|
350
|
+
const host = normalizeHost(evaluatorHost);
|
|
351
|
+
return host === null ? [] : [host];
|
|
352
|
+
}
|
|
353
|
+
function normalizeHost(evaluatorHost) {
|
|
354
|
+
const host = evaluatorHost?.trim() ?? "";
|
|
355
|
+
return host.length === 0 ? null : host;
|
|
356
|
+
}
|
|
258
357
|
var CONFIG_URL_KEY = "config:evaluator_url";
|
|
259
358
|
var CONFIG_TOKEN_KEY = "config:eval_token";
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
359
|
+
var SETTINGS_TOKEN_KEY = "eval_token";
|
|
360
|
+
var SETTINGS_BLOCK_MAPPINGS_KEY = "block_mappings";
|
|
361
|
+
var SETTINGS_CONTENT_FIELDS_KEY = "content_fields_by_collection";
|
|
362
|
+
var PLUGIN_SETTINGS_SCHEMA = {
|
|
363
|
+
[SETTINGS_CONTENT_FIELDS_KEY]: {
|
|
364
|
+
type: "string",
|
|
365
|
+
label: "Content fields by collection (JSON)",
|
|
366
|
+
description: "Ordered rendered body fields per collection.",
|
|
367
|
+
multiline: true
|
|
368
|
+
},
|
|
369
|
+
[SETTINGS_BLOCK_MAPPINGS_KEY]: {
|
|
370
|
+
type: "string",
|
|
371
|
+
label: "Custom block mappings (JSON)",
|
|
372
|
+
description: "Ordered mappings for the rendered fields of each custom block type.",
|
|
373
|
+
multiline: true
|
|
374
|
+
},
|
|
375
|
+
[SETTINGS_TOKEN_KEY]: {
|
|
376
|
+
type: "secret",
|
|
377
|
+
label: "EVAL_TOKEN",
|
|
378
|
+
description: "Bearer token for your aexeo-crawl-worker Worker. Encrypted at rest by the emdash host."
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
async function readSidecarConfig(store) {
|
|
382
|
+
const url = await store.kv.get(CONFIG_URL_KEY);
|
|
383
|
+
const token = await readStoredToken(store);
|
|
263
384
|
if (typeof url !== "string" || typeof token !== "string") {
|
|
264
385
|
return null;
|
|
265
386
|
}
|
|
@@ -268,48 +389,185 @@ async function readSidecarConfig(kv) {
|
|
|
268
389
|
}
|
|
269
390
|
return { url, token };
|
|
270
391
|
}
|
|
271
|
-
async function writeSidecarConfig(
|
|
272
|
-
await
|
|
273
|
-
await kv.set(
|
|
392
|
+
async function writeSidecarConfig(store, config) {
|
|
393
|
+
await writeStoredToken(store, config.token);
|
|
394
|
+
await store.kv.set(CONFIG_URL_KEY, config.url);
|
|
395
|
+
}
|
|
396
|
+
async function isTokenStoredInPlaintext(store) {
|
|
397
|
+
return await readLegacyToken(store) !== null;
|
|
398
|
+
}
|
|
399
|
+
async function readLegacyToken(store) {
|
|
400
|
+
const legacy = await store.kv.get(CONFIG_TOKEN_KEY);
|
|
401
|
+
return typeof legacy === "string" && legacy.length > 0 ? legacy : null;
|
|
402
|
+
}
|
|
403
|
+
async function readStoredToken(store) {
|
|
404
|
+
const legacy = await readLegacyToken(store);
|
|
405
|
+
if (legacy !== null) {
|
|
406
|
+
return legacy;
|
|
407
|
+
}
|
|
408
|
+
const { settings } = store;
|
|
409
|
+
if (settings !== void 0) {
|
|
410
|
+
try {
|
|
411
|
+
const stored = await settings.get(SETTINGS_TOKEN_KEY);
|
|
412
|
+
if (typeof stored === "string" && stored.length > 0) {
|
|
413
|
+
return stored;
|
|
414
|
+
}
|
|
415
|
+
} catch (err) {
|
|
416
|
+
store.log?.warn?.(
|
|
417
|
+
`aexeo: could not read the encrypted EVAL_TOKEN from plugin settings: ${describeError(err, "")}`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
var TokenStorageError = class extends Error {
|
|
424
|
+
};
|
|
425
|
+
async function writeStoredToken(store, token) {
|
|
426
|
+
const { settings } = store;
|
|
427
|
+
if (settings === void 0) {
|
|
428
|
+
throw new TokenStorageError(
|
|
429
|
+
"This emdash host does not provide encrypted plugin settings, so EVAL_TOKEN was not saved. Upgrade emdash."
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
await settings.set(SETTINGS_TOKEN_KEY, token);
|
|
434
|
+
await store.kv.delete(CONFIG_TOKEN_KEY);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
throw new TokenStorageError(
|
|
437
|
+
`EVAL_TOKEN was not saved because the host could not store it encrypted. Set EMDASH_ENCRYPTION_KEY on the emdash host and try again. Detail: ${describeError(err, token)}`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function describeError(err, secret) {
|
|
442
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
443
|
+
return secret.length === 0 ? raw : raw.split(secret).join("[redacted]");
|
|
444
|
+
}
|
|
445
|
+
function recordContentCoverage(ctx, documents) {
|
|
446
|
+
const counts = { documents: documents.length, complete: 0, partial: 0, unavailable: 0 };
|
|
447
|
+
for (const document of documents) {
|
|
448
|
+
if (document.body === void 0 || document.body_capabilities?.length === 0) counts.unavailable++;
|
|
449
|
+
else if (document.body_capabilities === void 0 || ["text", "headings", "paragraphs", "links"].every(
|
|
450
|
+
(capability) => document.body_capabilities?.includes(capability)
|
|
451
|
+
)) counts.complete++;
|
|
452
|
+
else counts.partial++;
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
ctx.log?.info?.("aexeo.content_coverage", counts);
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async function handleAfterDelete(event, ctx) {
|
|
460
|
+
for (const entry of await ctx.kv.list("document:")) {
|
|
461
|
+
const stored = toStoredDocument(entry.value);
|
|
462
|
+
if (stored !== null && stored.meta.id === event.id && stored.meta.collection === event.collection) {
|
|
463
|
+
await ctx.kv.delete(entry.key);
|
|
464
|
+
await ctx.kv.delete(findingsKey(stored.document.route));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async function readSandboxBlockMappings(ctx) {
|
|
469
|
+
if (!ctx.settings) return void 0;
|
|
470
|
+
try {
|
|
471
|
+
const raw = await ctx.settings.get(SETTINGS_BLOCK_MAPPINGS_KEY);
|
|
472
|
+
if (raw === null || raw === "") return void 0;
|
|
473
|
+
if (typeof raw !== "string" || raw.length > 65536) throw new Error("invalid mapping size or type");
|
|
474
|
+
const value = JSON.parse(raw);
|
|
475
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("mapping must be an object");
|
|
476
|
+
return value;
|
|
477
|
+
} catch {
|
|
478
|
+
ctx.log?.warn?.("Aexeo custom block mappings unavailable; custom content remains unverified");
|
|
479
|
+
return void 0;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async function readSandboxContentFields(ctx) {
|
|
483
|
+
if (!ctx.settings) return void 0;
|
|
484
|
+
try {
|
|
485
|
+
const raw = await ctx.settings.get(SETTINGS_CONTENT_FIELDS_KEY);
|
|
486
|
+
if (raw === null || raw === "") return void 0;
|
|
487
|
+
if (typeof raw !== "string" || raw.length > 65536) throw new Error("invalid selection size or type");
|
|
488
|
+
const value = JSON.parse(raw);
|
|
489
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || !Object.values(value).every((fields) => Array.isArray(fields) && fields.length > 0 && fields.every((field) => typeof field === "string" && field.length > 0))) throw new Error("invalid selection");
|
|
490
|
+
return value;
|
|
491
|
+
} catch {
|
|
492
|
+
ctx.log?.warn?.("Aexeo content field selection unavailable; multiple fields remain unverified");
|
|
493
|
+
return void 0;
|
|
494
|
+
}
|
|
274
495
|
}
|
|
275
496
|
async function handleAfterSave(event, ctx) {
|
|
276
|
-
const runtime = await readSidecarConfig(ctx
|
|
497
|
+
const runtime = await readSidecarConfig(ctx);
|
|
277
498
|
if (runtime === null) {
|
|
278
499
|
ctx.log?.warn?.(
|
|
279
500
|
"Aexeo afterSave skipped: configure the evaluator URL and token on the Setup page"
|
|
280
501
|
);
|
|
281
502
|
return;
|
|
282
503
|
}
|
|
504
|
+
const [blockMappings, fields] = await Promise.all([readSandboxBlockMappings(ctx), readSandboxContentFields(ctx)]);
|
|
283
505
|
await handleAfterSaveConfigured(
|
|
284
506
|
event,
|
|
285
507
|
ctx,
|
|
286
|
-
(documents) => evaluateViaSidecar(
|
|
508
|
+
(documents, context) => evaluateViaSidecar(
|
|
287
509
|
ctx.http,
|
|
288
|
-
{ url: runtime.url, authToken: runtime.token },
|
|
289
|
-
documents
|
|
290
|
-
|
|
510
|
+
{ url: runtime.url, authToken: runtime.token, log: ctx.log },
|
|
511
|
+
documents,
|
|
512
|
+
context?.collections
|
|
513
|
+
),
|
|
514
|
+
void 0,
|
|
515
|
+
blockMappings,
|
|
516
|
+
fields?.[event.content.type]
|
|
291
517
|
);
|
|
292
518
|
}
|
|
293
|
-
|
|
519
|
+
var SITE_GRAPH_RULE_IDS = /* @__PURE__ */ new Set([
|
|
520
|
+
"LNK001",
|
|
521
|
+
// internal link target missing from the site
|
|
522
|
+
"LNK002",
|
|
523
|
+
// no inbound links
|
|
524
|
+
"LNK003",
|
|
525
|
+
// weak anchor, checked only when the target route exists
|
|
526
|
+
"LNK004",
|
|
527
|
+
// too few inbound links
|
|
528
|
+
"SEO009",
|
|
529
|
+
// hreflang alternate target missing from the site
|
|
530
|
+
"SEO012",
|
|
531
|
+
// hreflang alternate does not link back
|
|
532
|
+
"SEO016",
|
|
533
|
+
// canonical points at another crawlable route
|
|
534
|
+
"SEO017",
|
|
535
|
+
// title and description shared with another route
|
|
536
|
+
"CNT005"
|
|
537
|
+
// visible text duplicated on another route
|
|
538
|
+
]);
|
|
539
|
+
async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilter, blockMappings, contentFields) {
|
|
294
540
|
const adapted = await adaptContentItemWithContext(
|
|
295
541
|
event.content,
|
|
296
542
|
ctx.content,
|
|
297
|
-
ctx.site?.locale
|
|
543
|
+
ctx.site?.locale,
|
|
544
|
+
blockMappings,
|
|
545
|
+
contentFields
|
|
298
546
|
);
|
|
299
547
|
const document = adapted.document;
|
|
548
|
+
recordContentCoverage(ctx, [document]);
|
|
300
549
|
const { kv, log } = ctx;
|
|
301
550
|
const stored = { document, meta: adapted.meta };
|
|
302
|
-
await persistDocument(ctx, stored)
|
|
303
|
-
|
|
551
|
+
if (!await persistDocument(ctx, stored)) {
|
|
552
|
+
log?.info?.(`Aexeo afterSave skipped: ${document.route} has a newer save`);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const result = await evaluator([document], { collections: [adapted.meta.collection] });
|
|
304
556
|
if (!result.ok) {
|
|
305
557
|
log?.error?.(`afterSave evaluator failure (${result.reason})`, {
|
|
306
558
|
detail: result.detail
|
|
307
559
|
});
|
|
308
560
|
return;
|
|
309
561
|
}
|
|
310
|
-
const
|
|
311
|
-
(finding) => finding.
|
|
562
|
+
const storedGraphFindings = (await readFindings(kv, document.route)).filter(
|
|
563
|
+
(finding) => SITE_GRAPH_RULE_IDS.has(finding.rule_id)
|
|
312
564
|
);
|
|
565
|
+
const pageFindings = [
|
|
566
|
+
...result.findings.filter(
|
|
567
|
+
(finding) => finding.scope === "page" && !SITE_GRAPH_RULE_IDS.has(finding.rule_id)
|
|
568
|
+
),
|
|
569
|
+
...storedGraphFindings
|
|
570
|
+
];
|
|
313
571
|
const filtered = suppressionFilter === void 0 ? pageFindings : suppressionFilter.apply(
|
|
314
572
|
{
|
|
315
573
|
route: document.route,
|
|
@@ -318,6 +576,10 @@ async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilte
|
|
|
318
576
|
},
|
|
319
577
|
pageFindings
|
|
320
578
|
);
|
|
579
|
+
if (await isSuperseded(kv, document.route, adapted.meta.updatedAt)) {
|
|
580
|
+
log?.info?.(`Aexeo afterSave discarded: ${document.route} changed while evaluating`);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
321
583
|
await kv.set(findingsKey(document.route), {
|
|
322
584
|
route: document.route,
|
|
323
585
|
findings: filtered
|
|
@@ -334,7 +596,9 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
334
596
|
totalFindings: 0,
|
|
335
597
|
errors: []
|
|
336
598
|
};
|
|
599
|
+
const storedAtStart = await readAllStoredDocuments(kv);
|
|
337
600
|
const documents = [];
|
|
601
|
+
const documentCollections = [];
|
|
338
602
|
const adaptedByRoute = /* @__PURE__ */ new Map();
|
|
339
603
|
const documentRoutes = /* @__PURE__ */ new Set();
|
|
340
604
|
for (const collection of collections) {
|
|
@@ -349,9 +613,12 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
349
613
|
const adapted = await adaptContentItemWithContext(
|
|
350
614
|
item,
|
|
351
615
|
ctx.content,
|
|
352
|
-
ctx.site?.locale
|
|
616
|
+
ctx.site?.locale,
|
|
617
|
+
options.blockMappings,
|
|
618
|
+
options.contentFieldsByCollection?.[collection]
|
|
353
619
|
);
|
|
354
620
|
documents.push(adapted.document);
|
|
621
|
+
documentCollections.push(adapted.meta.collection);
|
|
355
622
|
documentRoutes.add(adapted.document.route);
|
|
356
623
|
adaptedByRoute.set(adapted.document.route, {
|
|
357
624
|
document: adapted.document,
|
|
@@ -367,10 +634,14 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
367
634
|
} while (cursor !== void 0);
|
|
368
635
|
}
|
|
369
636
|
summary.documentsScanned = documents.length;
|
|
637
|
+
recordContentCoverage(ctx, documents);
|
|
370
638
|
for (const stored of adaptedByRoute.values()) {
|
|
371
|
-
await kv
|
|
639
|
+
const current = await readStoredDocument(kv, stored.document.route);
|
|
640
|
+
if (!isOlderRevision(stored.meta.updatedAt, current?.meta.updatedAt)) {
|
|
641
|
+
await kv.set(documentKey(stored.document.route), stored);
|
|
642
|
+
}
|
|
372
643
|
}
|
|
373
|
-
const result = await options.evaluator(documents);
|
|
644
|
+
const result = await options.evaluator(documents, { collections: documentCollections });
|
|
374
645
|
if (!result.ok) {
|
|
375
646
|
log?.error?.(`aexeo evaluator failure (${result.reason})`, {
|
|
376
647
|
detail: result.detail
|
|
@@ -378,19 +649,22 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
378
649
|
summary.errors.push(`${result.reason}: ${result.detail}`);
|
|
379
650
|
return summary;
|
|
380
651
|
}
|
|
381
|
-
const
|
|
652
|
+
const findingsByRoute = /* @__PURE__ */ new Map();
|
|
382
653
|
for (const route of documentRoutes) {
|
|
383
|
-
|
|
654
|
+
findingsByRoute.set(route, []);
|
|
384
655
|
}
|
|
385
656
|
for (const finding of result.findings) {
|
|
386
657
|
const bucket = finding.scope === "page" ? finding.path : "*";
|
|
387
|
-
const list =
|
|
658
|
+
const list = findingsByRoute.get(bucket) ?? [];
|
|
388
659
|
list.push(finding);
|
|
389
|
-
|
|
660
|
+
findingsByRoute.set(bucket, list);
|
|
390
661
|
}
|
|
391
662
|
let totalAfterSuppression = 0;
|
|
392
|
-
for (const [route, findings] of
|
|
663
|
+
for (const [route, findings] of findingsByRoute) {
|
|
393
664
|
const stored = adaptedByRoute.get(route);
|
|
665
|
+
if (stored !== void 0 && await isSuperseded(kv, route, stored.meta.updatedAt)) {
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
394
668
|
const context = {
|
|
395
669
|
route,
|
|
396
670
|
...stored?.meta.collection === void 0 ? {} : { collection: stored.meta.collection },
|
|
@@ -403,29 +677,52 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
403
677
|
}
|
|
404
678
|
summary.totalFindings = totalAfterSuppression;
|
|
405
679
|
if (summary.errors.length === 0) {
|
|
406
|
-
await removeStaleDocuments(ctx, collections, documentRoutes);
|
|
680
|
+
await removeStaleDocuments(ctx, collections, documentRoutes, storedAtStart);
|
|
407
681
|
}
|
|
408
682
|
return summary;
|
|
409
683
|
}
|
|
684
|
+
function isOlderRevision(revision, other) {
|
|
685
|
+
if (revision === void 0 || other === void 0) return false;
|
|
686
|
+
return Date.parse(revision) < Date.parse(other);
|
|
687
|
+
}
|
|
688
|
+
async function readStoredDocument(kv, route) {
|
|
689
|
+
return toStoredDocument(await kv.get(documentKey(route)));
|
|
690
|
+
}
|
|
691
|
+
async function isSuperseded(kv, route, revision) {
|
|
692
|
+
const current = await readStoredDocument(kv, route);
|
|
693
|
+
return current === null || isOlderRevision(revision, current.meta.updatedAt);
|
|
694
|
+
}
|
|
410
695
|
async function persistDocument(ctx, stored) {
|
|
411
696
|
const route = stored.document.route;
|
|
412
|
-
const
|
|
413
|
-
for (const entry of entries) {
|
|
697
|
+
const sameEntry = (await ctx.kv.list("document:")).flatMap((entry) => {
|
|
414
698
|
const previous = toStoredDocument(entry.value);
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
699
|
+
return previous !== null && previous.meta.id === stored.meta.id && previous.meta.collection === stored.meta.collection ? [{ key: entry.key, previous }] : [];
|
|
700
|
+
});
|
|
701
|
+
if (sameEntry.some(
|
|
702
|
+
({ previous }) => isOlderRevision(stored.meta.updatedAt, previous.meta.updatedAt)
|
|
703
|
+
)) {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
for (const { key, previous } of sameEntry) {
|
|
707
|
+
if (previous.document.route === route) continue;
|
|
708
|
+
await ctx.kv.delete(key);
|
|
419
709
|
await ctx.kv.delete(findingsKey(previous.document.route));
|
|
420
710
|
}
|
|
421
711
|
await ctx.kv.set(documentKey(route), stored);
|
|
712
|
+
return true;
|
|
422
713
|
}
|
|
423
|
-
async function removeStaleDocuments(ctx, collections, currentRoutes) {
|
|
714
|
+
async function removeStaleDocuments(ctx, collections, currentRoutes, storedAtStart) {
|
|
424
715
|
const scannedCollections = new Set(collections);
|
|
716
|
+
const revisionAtStart = new Map(
|
|
717
|
+
storedAtStart.map((stored) => [
|
|
718
|
+
stored.document.route,
|
|
719
|
+
stored.meta.updatedAt
|
|
720
|
+
])
|
|
721
|
+
);
|
|
425
722
|
const entries = await ctx.kv.list("document:");
|
|
426
723
|
for (const entry of entries) {
|
|
427
724
|
const stored = toStoredDocument(entry.value);
|
|
428
|
-
if (stored === null || !scannedCollections.has(stored.meta.collection) || currentRoutes.has(stored.document.route)) {
|
|
725
|
+
if (stored === null || stored.meta.collection !== "" && !scannedCollections.has(stored.meta.collection) || currentRoutes.has(stored.document.route) || !revisionAtStart.has(stored.document.route) || revisionAtStart.get(stored.document.route) !== stored.meta.updatedAt) {
|
|
429
726
|
continue;
|
|
430
727
|
}
|
|
431
728
|
await ctx.kv.delete(entry.key);
|
|
@@ -481,174 +778,111 @@ async function readFindings(kv, route) {
|
|
|
481
778
|
return stored.findings;
|
|
482
779
|
}
|
|
483
780
|
|
|
484
|
-
// src/
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
return {
|
|
490
|
-
overall_score: 0,
|
|
491
|
-
citation_readiness_score: 0,
|
|
492
|
-
truth_consistency_score: 0,
|
|
493
|
-
answer_pack_score: 0,
|
|
494
|
-
external_trust_alignment_score: null,
|
|
495
|
-
route_scores: [],
|
|
496
|
-
blockers: []
|
|
497
|
-
};
|
|
498
|
-
}
|
|
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
|
-
}
|
|
781
|
+
// src/sidecar-url.ts
|
|
782
|
+
function validateSidecarUrl(raw, allowedHosts = []) {
|
|
783
|
+
const url = raw.trim();
|
|
784
|
+
if (url.length === 0) {
|
|
785
|
+
return { ok: false, error: "Sidecar URL is required." };
|
|
511
786
|
}
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// src/indexnow.ts
|
|
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) {
|
|
787
|
+
let parsed;
|
|
547
788
|
try {
|
|
548
|
-
|
|
789
|
+
parsed = new URL(url);
|
|
549
790
|
} catch {
|
|
550
|
-
return
|
|
791
|
+
return { ok: false, error: `Sidecar URL is not a valid URL: ${url}` };
|
|
551
792
|
}
|
|
793
|
+
if (parsed.protocol !== "https:") {
|
|
794
|
+
return {
|
|
795
|
+
ok: false,
|
|
796
|
+
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}`
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
const host = parsed.hostname.toLowerCase();
|
|
800
|
+
if (host.endsWith(".")) {
|
|
801
|
+
return {
|
|
802
|
+
ok: false,
|
|
803
|
+
error: `Sidecar URL host must not end with a dot: ${host}`
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
if (isBlockedHost(host)) {
|
|
807
|
+
return {
|
|
808
|
+
ok: false,
|
|
809
|
+
error: "Sidecar URL must not point at a loopback, private, or link-local address. Use the deployed *.workers.dev URL."
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
if (allowedHosts.length > 0 && !hostMatchesAllowlist(host, allowedHosts)) {
|
|
813
|
+
return {
|
|
814
|
+
ok: false,
|
|
815
|
+
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.`
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
return { ok: true, url };
|
|
552
819
|
}
|
|
553
|
-
function
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
}
|
|
820
|
+
function isBlockedHost(host) {
|
|
821
|
+
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
if (host.startsWith("[")) {
|
|
825
|
+
return isBlockedIpv6(host);
|
|
567
826
|
}
|
|
568
|
-
return
|
|
827
|
+
return isBlockedIpv4(host);
|
|
569
828
|
}
|
|
570
|
-
function
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
829
|
+
function isBlockedIpv4(host) {
|
|
830
|
+
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
831
|
+
if (match === null) {
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
const first = Number(match[1]);
|
|
835
|
+
const second = Number(match[2]);
|
|
836
|
+
if (first === 0) return true;
|
|
837
|
+
if (first === 127) return true;
|
|
838
|
+
if (first === 10) return true;
|
|
839
|
+
if (first === 172 && second >= 16 && second <= 31) return true;
|
|
840
|
+
if (first === 192 && second === 168) return true;
|
|
841
|
+
if (first === 169 && second === 254) return true;
|
|
842
|
+
if (first === 100 && second >= 64 && second <= 127) return true;
|
|
843
|
+
if (first >= 224) return true;
|
|
844
|
+
return false;
|
|
579
845
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
};
|
|
846
|
+
function isBlockedIpv6(host) {
|
|
847
|
+
const inner = host.slice(1, -1).toLowerCase();
|
|
848
|
+
if (inner === "::" || inner === "::1") {
|
|
849
|
+
return true;
|
|
618
850
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
};
|
|
646
|
-
|
|
851
|
+
if (inner.startsWith("::ffff:") || inner.startsWith("::7f")) {
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
const firstHextet = Number.parseInt(inner.split(":")[0] ?? "", 16);
|
|
855
|
+
if (Number.isNaN(firstHextet)) {
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
if ((firstHextet & 65024) === 64512) {
|
|
859
|
+
return true;
|
|
860
|
+
}
|
|
861
|
+
if ((firstHextet & 65472) === 65152) {
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
function hostMatchesAllowlist(host, allowedHosts) {
|
|
867
|
+
return allowedHosts.some((allowed) => {
|
|
868
|
+
const entry = allowed.trim().toLowerCase();
|
|
869
|
+
if (entry.length === 0) {
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
if (entry.startsWith("*.")) {
|
|
873
|
+
const suffix = entry.slice(1);
|
|
874
|
+
return host.endsWith(suffix) && host.length > suffix.length;
|
|
875
|
+
}
|
|
876
|
+
return host === entry;
|
|
877
|
+
});
|
|
878
|
+
}
|
|
647
879
|
|
|
648
|
-
// src/sandbox
|
|
649
|
-
function
|
|
650
|
-
return
|
|
880
|
+
// src/evaluator-sandbox.ts
|
|
881
|
+
async function scoreSite(_documents) {
|
|
882
|
+
return null;
|
|
651
883
|
}
|
|
884
|
+
|
|
885
|
+
// src/findings-view.ts
|
|
652
886
|
function normalizeInteraction(input) {
|
|
653
887
|
if (!input || typeof input !== "object") {
|
|
654
888
|
return { type: "page_load", page: "/findings" };
|
|
@@ -678,107 +912,33 @@ function normalizeInteraction(input) {
|
|
|
678
912
|
}
|
|
679
913
|
return { type: "page_load", page: "/findings" };
|
|
680
914
|
}
|
|
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
|
-
};
|
|
915
|
+
async function handleRefresh(options) {
|
|
762
916
|
let summary;
|
|
763
917
|
try {
|
|
764
|
-
summary = await evaluateAndPersistAll(
|
|
765
|
-
|
|
918
|
+
summary = await evaluateAndPersistAll(options.ctx, {
|
|
919
|
+
collections: options.collections,
|
|
920
|
+
evaluator: options.evaluator,
|
|
921
|
+
blockMappings: options.blockMappings,
|
|
922
|
+
contentFieldsByCollection: options.contentFieldsByCollection,
|
|
923
|
+
// Always forwarded. See RefreshOptions.suppressionFilter.
|
|
924
|
+
suppressionFilter: options.suppressionFilter
|
|
766
925
|
});
|
|
767
926
|
} catch (err) {
|
|
768
927
|
const detail = err instanceof Error ? err.message : String(err);
|
|
928
|
+
const message = `Refresh failed: ${detail}`;
|
|
769
929
|
return {
|
|
770
930
|
blocks: [
|
|
771
931
|
{ type: "header", text: "SEO findings" },
|
|
772
932
|
{
|
|
773
933
|
type: "banner",
|
|
774
|
-
title:
|
|
775
|
-
variant:
|
|
934
|
+
title: message,
|
|
935
|
+
variant: options.tuning.refreshFailureVariant
|
|
776
936
|
}
|
|
777
937
|
],
|
|
778
|
-
toast: { message
|
|
938
|
+
toast: { message, type: "error" }
|
|
779
939
|
};
|
|
780
940
|
}
|
|
781
|
-
const refreshed = await renderFindingsPage(ctx);
|
|
941
|
+
const refreshed = await renderFindingsPage(options.ctx.kv, options.tuning);
|
|
782
942
|
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
943
|
if (summary.errors.length > 0) {
|
|
784
944
|
refreshed.blocks.unshift({
|
|
@@ -795,10 +955,43 @@ async function handleRefresh(ctx) {
|
|
|
795
955
|
}
|
|
796
956
|
};
|
|
797
957
|
}
|
|
798
|
-
async function
|
|
799
|
-
const
|
|
958
|
+
async function readAllFindings(kv) {
|
|
959
|
+
const entries = await kv.list(
|
|
960
|
+
"findings:"
|
|
961
|
+
);
|
|
962
|
+
const out = [];
|
|
963
|
+
for (const entry of entries) {
|
|
964
|
+
if (entry.value === null) {
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const route = entry.key.replace(/^findings:/, "");
|
|
968
|
+
for (const finding of entry.value.findings) {
|
|
969
|
+
out.push({ ...finding, document_route: route });
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return out;
|
|
973
|
+
}
|
|
974
|
+
function severityFirst(a, b) {
|
|
975
|
+
const rank = (severity) => severity === "error" ? 0 : 1;
|
|
976
|
+
const diff = rank(a.severity) - rank(b.severity);
|
|
977
|
+
if (diff !== 0) {
|
|
978
|
+
return diff;
|
|
979
|
+
}
|
|
980
|
+
return a.rule_id.localeCompare(b.rule_id);
|
|
981
|
+
}
|
|
982
|
+
function uniqueRoutes(rows) {
|
|
983
|
+
const routes = /* @__PURE__ */ new Set();
|
|
984
|
+
for (const row of rows) {
|
|
985
|
+
routes.add(row.document_route);
|
|
986
|
+
}
|
|
987
|
+
return [...routes].sort();
|
|
988
|
+
}
|
|
989
|
+
async function renderFindingsPage(kv, tuning) {
|
|
990
|
+
const findings = await readAllFindings(kv);
|
|
800
991
|
const errors = findings.filter((finding) => finding.severity === "error");
|
|
801
|
-
const warnings = findings.filter(
|
|
992
|
+
const warnings = findings.filter(
|
|
993
|
+
(finding) => finding.severity === "warning"
|
|
994
|
+
);
|
|
802
995
|
const sorted = [...findings].sort(severityFirst);
|
|
803
996
|
const routes = uniqueRoutes(findings);
|
|
804
997
|
const blocks = [
|
|
@@ -812,8 +1005,8 @@ async function renderFindingsPage(ctx) {
|
|
|
812
1005
|
type: "actions",
|
|
813
1006
|
elements: [
|
|
814
1007
|
// Refresh evaluates every configured entry. Pressing it lists
|
|
815
|
-
// content via the live in-request bridge,
|
|
816
|
-
//
|
|
1008
|
+
// content via the live in-request bridge, evaluates it, and
|
|
1009
|
+
// writes findings back to KV (with suppressions applied).
|
|
817
1010
|
{
|
|
818
1011
|
type: "button",
|
|
819
1012
|
label: "Refresh",
|
|
@@ -831,7 +1024,7 @@ async function renderFindingsPage(ctx) {
|
|
|
831
1024
|
},
|
|
832
1025
|
sorted.length === 0 ? {
|
|
833
1026
|
type: "context",
|
|
834
|
-
text:
|
|
1027
|
+
text: tuning.emptyFindingsText
|
|
835
1028
|
} : findingsTable(sorted)
|
|
836
1029
|
];
|
|
837
1030
|
if (routes.length > 0) {
|
|
@@ -850,37 +1043,6 @@ async function renderFindingsPage(ctx) {
|
|
|
850
1043
|
}
|
|
851
1044
|
return { blocks };
|
|
852
1045
|
}
|
|
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
1046
|
function findingsTable(rows) {
|
|
885
1047
|
return {
|
|
886
1048
|
type: "table",
|
|
@@ -898,50 +1060,61 @@ function findingsTable(rows) {
|
|
|
898
1060
|
}))
|
|
899
1061
|
};
|
|
900
1062
|
}
|
|
901
|
-
async function renderScoreWidget(
|
|
902
|
-
const documents = await readAllDocuments(
|
|
1063
|
+
async function renderScoreWidget(kv, tuning, log) {
|
|
1064
|
+
const documents = await readAllDocuments(kv);
|
|
903
1065
|
if (documents.length === 0) {
|
|
904
1066
|
return {
|
|
905
1067
|
blocks: [
|
|
906
1068
|
{ type: "header", text: "SEO score" },
|
|
907
1069
|
{
|
|
908
1070
|
type: "context",
|
|
909
|
-
text:
|
|
1071
|
+
text: tuning.emptyScoreText
|
|
1072
|
+
}
|
|
1073
|
+
]
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
let score;
|
|
1077
|
+
try {
|
|
1078
|
+
score = await tuning.score(documents, kv);
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
log?.error("Aexeo score widget failed to initialize", { error });
|
|
1081
|
+
return {
|
|
1082
|
+
blocks: [
|
|
1083
|
+
{ type: "banner", title: "SEO score is temporarily unavailable", variant: "alert" },
|
|
1084
|
+
{ type: "context", text: "The authoring interface remains available. Retry after the evaluator runtime has been checked." }
|
|
1085
|
+
]
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
if (score === null) {
|
|
1089
|
+
return {
|
|
1090
|
+
blocks: [
|
|
1091
|
+
{ type: "header", text: "SEO score" },
|
|
1092
|
+
{
|
|
1093
|
+
type: "context",
|
|
1094
|
+
text: "The site score is unavailable in sandboxed mode. Findings are not affected."
|
|
910
1095
|
}
|
|
911
1096
|
]
|
|
912
1097
|
};
|
|
913
1098
|
}
|
|
914
|
-
const score = await scoreSite(documents);
|
|
915
1099
|
const blocks = [
|
|
916
1100
|
{
|
|
917
1101
|
type: "stats",
|
|
918
1102
|
items: [
|
|
919
|
-
{
|
|
920
|
-
label: "Overall",
|
|
921
|
-
value: `${score.overall_score}`
|
|
922
|
-
},
|
|
923
1103
|
{
|
|
924
1104
|
label: "Citation",
|
|
925
|
-
value:
|
|
1105
|
+
value: score.citation_readiness_score?.toString() ?? "unknown"
|
|
926
1106
|
},
|
|
927
1107
|
{
|
|
928
|
-
label:
|
|
929
|
-
value:
|
|
1108
|
+
label: tuning.truthLabel(score),
|
|
1109
|
+
value: score.truth_consistency_score?.toString() ?? "unknown"
|
|
930
1110
|
},
|
|
931
1111
|
{
|
|
932
1112
|
label: "Answers",
|
|
933
|
-
value: `${score.
|
|
1113
|
+
value: `${score.answer_pack.routes_with_answer_blocks}/${score.answer_pack.routes_analyzed} routes`
|
|
934
1114
|
}
|
|
935
1115
|
]
|
|
936
1116
|
}
|
|
937
1117
|
];
|
|
938
|
-
if (score.overall_score < 60) {
|
|
939
|
-
blocks.unshift({
|
|
940
|
-
type: "banner",
|
|
941
|
-
title: `Site score is ${score.overall_score} \u2014 below the 60 quality threshold`,
|
|
942
|
-
variant: "alert"
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
1118
|
if (score.blockers.length > 0) {
|
|
946
1119
|
blocks.push({
|
|
947
1120
|
type: "context",
|
|
@@ -957,7 +1130,7 @@ function topBlockersLine(score) {
|
|
|
957
1130
|
}
|
|
958
1131
|
return `Top blockers: ${top.join(" \u2022 ")}`;
|
|
959
1132
|
}
|
|
960
|
-
async function renderDocumentPanel(
|
|
1133
|
+
async function renderDocumentPanel(kv, route) {
|
|
961
1134
|
if (route === void 0) {
|
|
962
1135
|
return {
|
|
963
1136
|
blocks: [
|
|
@@ -969,9 +1142,11 @@ async function renderDocumentPanel(ctx, route) {
|
|
|
969
1142
|
]
|
|
970
1143
|
};
|
|
971
1144
|
}
|
|
972
|
-
const findings = await readFindings(
|
|
1145
|
+
const findings = await readFindings(kv, route);
|
|
973
1146
|
const errors = findings.filter((finding) => finding.severity === "error");
|
|
974
|
-
const warnings = findings.filter(
|
|
1147
|
+
const warnings = findings.filter(
|
|
1148
|
+
(finding) => finding.severity === "warning"
|
|
1149
|
+
);
|
|
975
1150
|
const sorted = [...findings].sort(severityFirst);
|
|
976
1151
|
return {
|
|
977
1152
|
blocks: [
|
|
@@ -1004,15 +1179,198 @@ function documentFindingsTable(findings) {
|
|
|
1004
1179
|
}))
|
|
1005
1180
|
};
|
|
1006
1181
|
}
|
|
1182
|
+
function notFound(page) {
|
|
1183
|
+
return {
|
|
1184
|
+
blocks: [
|
|
1185
|
+
{ type: "header", text: "Not found" },
|
|
1186
|
+
{ type: "context", text: `unknown page: ${page}` }
|
|
1187
|
+
]
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// src/suppressions.ts
|
|
1192
|
+
var NOOP_FILTER = {
|
|
1193
|
+
apply: (_context, findings) => [...findings]
|
|
1194
|
+
};
|
|
1195
|
+
function compileSuppressions(rules) {
|
|
1196
|
+
if (rules === void 0 || rules.length === 0) {
|
|
1197
|
+
return NOOP_FILTER;
|
|
1198
|
+
}
|
|
1199
|
+
rules.forEach((rule, index) => {
|
|
1200
|
+
const hasRoute = rule.routePattern !== void 0 && rule.routePattern.length > 0;
|
|
1201
|
+
const hasRuleIds = rule.ruleIds !== void 0 && rule.ruleIds.length > 0;
|
|
1202
|
+
const hasCollections = rule.collections !== void 0 && rule.collections.length > 0;
|
|
1203
|
+
const hasStatuses = rule.statuses !== void 0 && rule.statuses.length > 0;
|
|
1204
|
+
if (!hasRoute && !hasRuleIds && !hasCollections && !hasStatuses) {
|
|
1205
|
+
throw new Error(
|
|
1206
|
+
`aexeoPlugin: suppressions[${index}] is empty \u2014 set at least one of routePattern, ruleIds, collections, statuses`
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
const compiled = rules.map((rule) => ({
|
|
1211
|
+
matchRoute: rule.routePattern === void 0 ? matchAlways : globToMatcher(rule.routePattern),
|
|
1212
|
+
ruleIds: rule.ruleIds === void 0 || rule.ruleIds.length === 0 ? null : new Set(rule.ruleIds),
|
|
1213
|
+
collections: rule.collections === void 0 || rule.collections.length === 0 ? null : new Set(rule.collections),
|
|
1214
|
+
statuses: rule.statuses === void 0 || rule.statuses.length === 0 ? null : new Set(rule.statuses)
|
|
1215
|
+
}));
|
|
1216
|
+
return {
|
|
1217
|
+
apply(context, findings) {
|
|
1218
|
+
return findings.filter(
|
|
1219
|
+
(finding) => !compiled.some((rule) => {
|
|
1220
|
+
if (!rule.matchRoute(context.route)) return false;
|
|
1221
|
+
if (rule.ruleIds !== null && !rule.ruleIds.has(finding.rule_id)) {
|
|
1222
|
+
return false;
|
|
1223
|
+
}
|
|
1224
|
+
if (rule.collections !== null) {
|
|
1225
|
+
if (context.collection === void 0 || !rule.collections.has(context.collection)) {
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (rule.statuses !== null) {
|
|
1230
|
+
if (context.status === void 0 || !rule.statuses.has(context.status)) {
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return true;
|
|
1235
|
+
})
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
var matchAlways = () => true;
|
|
1241
|
+
function globToMatcher(pattern) {
|
|
1242
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1243
|
+
const DOUBLE_STAR_TOKEN = "";
|
|
1244
|
+
const body = escaped.replace(/\*\*/g, DOUBLE_STAR_TOKEN).replace(/\*/g, "[^/]*").replace(new RegExp(DOUBLE_STAR_TOKEN, "g"), ".*").replace(/\?/g, "[^/]");
|
|
1245
|
+
const re = new RegExp(`^${body}$`);
|
|
1246
|
+
return (route) => re.test(route);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// src/sandbox-entry.ts
|
|
1250
|
+
function definePlugin(plugin) {
|
|
1251
|
+
return plugin;
|
|
1252
|
+
}
|
|
1253
|
+
var sandboxSuppressionFilter = compileSuppressions(
|
|
1254
|
+
void 0
|
|
1255
|
+
);
|
|
1256
|
+
var adminViewTuning = {
|
|
1257
|
+
emptyFindingsText: "Once a document publishes, its rule findings list here.",
|
|
1258
|
+
emptyScoreText: "No documents saved yet \u2014 score appears after the first emdash save.",
|
|
1259
|
+
refreshFailureVariant: "alert",
|
|
1260
|
+
score: async (documents) => scoreSite(documents),
|
|
1261
|
+
truthLabel: () => "Truth"
|
|
1262
|
+
};
|
|
1263
|
+
async function handleAdminRoute(input, ctx) {
|
|
1264
|
+
const body = normalizeInteraction(input?.input);
|
|
1265
|
+
ctx.log?.info?.(
|
|
1266
|
+
`aexeo route: type=${body.type} page=${body.type === "page_load" ? body.page : ""}`
|
|
1267
|
+
);
|
|
1268
|
+
const dispatch = { body, kv: ctx.kv, ctx };
|
|
1269
|
+
if (body.type === "page_load") {
|
|
1270
|
+
return handlePageLoad(dispatch, body.page);
|
|
1271
|
+
}
|
|
1272
|
+
if (body.type === "block_action") {
|
|
1273
|
+
return handleBlockAction(
|
|
1274
|
+
dispatch,
|
|
1275
|
+
body.action_id,
|
|
1276
|
+
body.value
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
if (body.type === "form_submit") {
|
|
1280
|
+
return handleFormSubmit(
|
|
1281
|
+
dispatch,
|
|
1282
|
+
body.action_id,
|
|
1283
|
+
body.values
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
return handlePageLoad(dispatch, "findings");
|
|
1287
|
+
}
|
|
1288
|
+
async function handleFormSubmit(ctx, actionId, values) {
|
|
1289
|
+
if (actionId === "save_setup") {
|
|
1290
|
+
return handleSetupSubmit(ctx, values);
|
|
1291
|
+
}
|
|
1292
|
+
if (actionId === "view_document") {
|
|
1293
|
+
const picked = values["route_picker"];
|
|
1294
|
+
if (typeof picked === "string" && picked.length > 0) {
|
|
1295
|
+
return renderDocumentPanel(ctx.kv, picked);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
return handlePageLoad(ctx, "findings");
|
|
1299
|
+
}
|
|
1300
|
+
async function handlePageLoad(ctx, page) {
|
|
1301
|
+
const normalized = page.startsWith("/") ? page.slice(1) : page;
|
|
1302
|
+
if (normalized === "findings") {
|
|
1303
|
+
return renderFindingsPage(ctx.kv, adminViewTuning);
|
|
1304
|
+
}
|
|
1305
|
+
if (normalized === "widget:aexeo-score") {
|
|
1306
|
+
return renderScoreWidget(ctx.kv, adminViewTuning);
|
|
1307
|
+
}
|
|
1308
|
+
if (normalized === "document") {
|
|
1309
|
+
return renderDocumentPanel(ctx.kv);
|
|
1310
|
+
}
|
|
1311
|
+
if (normalized === "setup") {
|
|
1312
|
+
return renderSetupPage(ctx);
|
|
1313
|
+
}
|
|
1314
|
+
return notFound(page);
|
|
1315
|
+
}
|
|
1316
|
+
async function handleBlockAction(ctx, actionId, value) {
|
|
1317
|
+
if (actionId === "view_document" && typeof value === "string") {
|
|
1318
|
+
return renderDocumentPanel(ctx.kv, value);
|
|
1319
|
+
}
|
|
1320
|
+
if (actionId === "refresh_findings") {
|
|
1321
|
+
return handleRefresh2(ctx);
|
|
1322
|
+
}
|
|
1323
|
+
if (actionId.startsWith("filter:")) {
|
|
1324
|
+
return renderFindingsPage(ctx.kv, adminViewTuning);
|
|
1325
|
+
}
|
|
1326
|
+
return notFound(actionId);
|
|
1327
|
+
}
|
|
1328
|
+
async function handleRefresh2(ctx) {
|
|
1329
|
+
const sandboxEvaluator = async (documents, context) => {
|
|
1330
|
+
const runtime = await readSidecarConfig(ctx.ctx);
|
|
1331
|
+
if (runtime === null) {
|
|
1332
|
+
return {
|
|
1333
|
+
ok: false,
|
|
1334
|
+
reason: "config_missing",
|
|
1335
|
+
detail: "sidecar not configured \u2014 open the Aexeo Setup page and enter your evaluator URL and token"
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
return evaluateViaSidecar(
|
|
1339
|
+
ctx.ctx.http,
|
|
1340
|
+
{ url: runtime.url, authToken: runtime.token, log: ctx.ctx.log },
|
|
1341
|
+
documents,
|
|
1342
|
+
context?.collections
|
|
1343
|
+
);
|
|
1344
|
+
};
|
|
1345
|
+
return handleRefresh({
|
|
1346
|
+
ctx: ctx.ctx,
|
|
1347
|
+
// The sandboxed descriptor takes no collection knobs, so the sweep
|
|
1348
|
+
// always covers the default set.
|
|
1349
|
+
collections: DEFAULT_COLLECTIONS,
|
|
1350
|
+
evaluator: sandboxEvaluator,
|
|
1351
|
+
suppressionFilter: sandboxSuppressionFilter,
|
|
1352
|
+
blockMappings: await readSandboxBlockMappings(ctx.ctx),
|
|
1353
|
+
contentFieldsByCollection: await readSandboxContentFields(ctx.ctx),
|
|
1354
|
+
tuning: adminViewTuning
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1007
1357
|
async function renderSetupPage(ctx, options = {}) {
|
|
1008
|
-
const existing = await readSidecarConfig(ctx.
|
|
1358
|
+
const existing = await readSidecarConfig(ctx.ctx);
|
|
1359
|
+
const tokenInPlaintext = await isTokenStoredInPlaintext(ctx.ctx);
|
|
1009
1360
|
const blocks = [
|
|
1010
1361
|
{ type: "header", text: "Aexeo setup" },
|
|
1011
1362
|
{
|
|
1012
1363
|
type: "context",
|
|
1013
|
-
text: "Paste the URL and auth token of your deployed aexeo-crawl-worker.
|
|
1364
|
+
text: "Paste the URL and auth token of your deployed aexeo-crawl-worker. Saving stores the token encrypted by the emdash host, and fails if the host cannot encrypt it. 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
1365
|
}
|
|
1015
1366
|
];
|
|
1367
|
+
if (tokenInPlaintext) {
|
|
1368
|
+
blocks.push({
|
|
1369
|
+
type: "banner",
|
|
1370
|
+
title: "EVAL_TOKEN is currently stored unencrypted in plugin KV. Save this form to encrypt it.",
|
|
1371
|
+
variant: "alert"
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1016
1374
|
if (options.bannerError !== void 0) {
|
|
1017
1375
|
blocks.push({
|
|
1018
1376
|
type: "banner",
|
|
@@ -1053,44 +1411,28 @@ async function renderSetupPage(ctx, options = {}) {
|
|
|
1053
1411
|
{ type: "divider" },
|
|
1054
1412
|
{
|
|
1055
1413
|
type: "context",
|
|
1056
|
-
text: existing === null ? "Status: not configured. Refresh on the findings page will fail until both fields are saved." : `Status: configured. Sidecar at ${existing.url}. Token stored \u2014 paste a new one above to rotate.`
|
|
1414
|
+
text: existing === null ? "Status: not configured. Refresh on the findings page will fail until both fields are saved." : `Status: configured. Sidecar at ${existing.url}. Token stored ${tokenInPlaintext ? "unencrypted" : "encrypted"} \u2014 paste a new one above to rotate.`
|
|
1057
1415
|
}
|
|
1058
1416
|
);
|
|
1059
1417
|
return { blocks };
|
|
1060
1418
|
}
|
|
1419
|
+
function runtimeAllowedHosts() {
|
|
1420
|
+
const env = typeof process === "undefined" ? void 0 : process.env;
|
|
1421
|
+
return buildAllowedHosts(env?.["AEXEO_EVALUATOR_HOST"] ?? null);
|
|
1422
|
+
}
|
|
1061
1423
|
async function handleSetupSubmit(ctx, values) {
|
|
1062
1424
|
const rawUrl = values["evaluator_url"];
|
|
1063
1425
|
const rawToken = values["eval_token"];
|
|
1064
1426
|
const url = typeof rawUrl === "string" ? rawUrl.trim() : "";
|
|
1065
1427
|
const token = typeof rawToken === "string" ? rawToken.trim() : "";
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
bannerError: "Sidecar URL is required.",
|
|
1069
|
-
initialUrl: url
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
let parsed;
|
|
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)) {
|
|
1428
|
+
const validated = validateSidecarUrl(url, runtimeAllowedHosts());
|
|
1429
|
+
if (!validated.ok) {
|
|
1088
1430
|
return renderSetupPage(ctx, {
|
|
1089
|
-
bannerError:
|
|
1431
|
+
bannerError: validated.error,
|
|
1090
1432
|
initialUrl: url
|
|
1091
1433
|
});
|
|
1092
1434
|
}
|
|
1093
|
-
const existing = await readSidecarConfig(ctx.
|
|
1435
|
+
const existing = await readSidecarConfig(ctx.ctx);
|
|
1094
1436
|
let resolvedToken;
|
|
1095
1437
|
if (token.length > 0) {
|
|
1096
1438
|
resolvedToken = token;
|
|
@@ -1099,34 +1441,34 @@ async function handleSetupSubmit(ctx, values) {
|
|
|
1099
1441
|
} else {
|
|
1100
1442
|
return renderSetupPage(ctx, {
|
|
1101
1443
|
bannerError: "EVAL_TOKEN is required on first setup.",
|
|
1102
|
-
initialUrl: url
|
|
1444
|
+
initialUrl: validated.url
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
const next = {
|
|
1448
|
+
url: validated.url,
|
|
1449
|
+
token: resolvedToken
|
|
1450
|
+
};
|
|
1451
|
+
try {
|
|
1452
|
+
await writeSidecarConfig(ctx.ctx, next);
|
|
1453
|
+
} catch (err) {
|
|
1454
|
+
if (!(err instanceof TokenStorageError)) throw err;
|
|
1455
|
+
return renderSetupPage(ctx, {
|
|
1456
|
+
bannerError: err.message,
|
|
1457
|
+
initialUrl: validated.url
|
|
1103
1458
|
});
|
|
1104
1459
|
}
|
|
1105
|
-
const next = { url, token: resolvedToken };
|
|
1106
|
-
await writeSidecarConfig(ctx.kv, next);
|
|
1107
1460
|
return renderSetupPage(ctx, {
|
|
1108
1461
|
bannerSuccess: "Configuration saved. Click Refresh on the findings page to evaluate."
|
|
1109
1462
|
});
|
|
1110
1463
|
}
|
|
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
1464
|
var sandbox_entry_default = definePlugin({
|
|
1120
1465
|
hooks: {
|
|
1121
|
-
"content:afterSave": handleAfterSave
|
|
1466
|
+
"content:afterSave": handleAfterSave,
|
|
1467
|
+
"content:afterDelete": handleAfterDelete
|
|
1122
1468
|
},
|
|
1123
1469
|
routes: {
|
|
1124
1470
|
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
|
|
1471
|
+
}
|
|
1130
1472
|
});
|
|
1131
1473
|
export {
|
|
1132
1474
|
sandbox_entry_default as default
|