@aeptus/aexeo-emdash 0.8.17 → 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 +129 -0
- package/INSTALL.md +22 -14
- package/README.md +6 -3
- package/dist/adapter.d.ts +32 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/configured.d.ts.map +1 -1
- package/dist/configured.js +550 -344
- package/dist/data-route.d.ts.map +1 -1
- package/dist/findings-view.d.ts +136 -0
- package/dist/findings-view.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -23
- package/dist/plugin.d.ts +25 -3
- package/dist/plugin.d.ts.map +1 -1
- package/dist/sandbox-entry.d.ts +2 -22
- package/dist/sandbox-entry.d.ts.map +1 -1
- package/dist/sandbox-entry.js +745 -480
- 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.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +11 -5
- package/wasm/aexeo_emdash_bridge_bg.wasm +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
|
@@ -1,13 +1,40 @@
|
|
|
1
1
|
// src/adapter.ts
|
|
2
|
-
function
|
|
3
|
-
const
|
|
2
|
+
async function adaptContentItemWithContext(content, contentApi, defaultLocale) {
|
|
3
|
+
const publicUrl = content.status === "published" ? await contentApi.getPublicUrl(content.type, content.id) : null;
|
|
4
|
+
const route = publicUrl === null ? deriveRoute(content) : routeFromPublicUrl(publicUrl);
|
|
5
|
+
const adapted = adaptContentItem(content, {
|
|
6
|
+
route,
|
|
7
|
+
publicUrl,
|
|
8
|
+
...defaultLocale === void 0 ? {} : { defaultLocale }
|
|
9
|
+
});
|
|
10
|
+
const translationSet = await contentApi.getTranslations(
|
|
11
|
+
content.type,
|
|
12
|
+
content.id
|
|
13
|
+
);
|
|
14
|
+
const alternates = await Promise.all(
|
|
15
|
+
translationSet.translations.filter(
|
|
16
|
+
(translation) => translation.status === "published" && typeof translation.locale === "string" && translation.locale.length > 0
|
|
17
|
+
).map(async (translation) => {
|
|
18
|
+
const href = translation.id === content.id ? publicUrl : await contentApi.getPublicUrl(content.type, translation.id);
|
|
19
|
+
if (href === null) return null;
|
|
20
|
+
return { lang: translation.locale, href };
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
const uniqueAlternates = /* @__PURE__ */ new Map();
|
|
24
|
+
for (const alternate of alternates) {
|
|
25
|
+
if (alternate !== null) uniqueAlternates.set(alternate.lang, alternate);
|
|
26
|
+
}
|
|
27
|
+
if (uniqueAlternates.size > 0) {
|
|
28
|
+
adapted.document.alternates = [...uniqueAlternates.values()];
|
|
29
|
+
}
|
|
30
|
+
return adapted;
|
|
31
|
+
}
|
|
32
|
+
function adaptContentItem(content, options = {}) {
|
|
33
|
+
const route = options.route ?? deriveRoute(content);
|
|
4
34
|
const title = stringOrEmpty(
|
|
5
35
|
content.seo?.title ?? content.data["title"] ?? content.slug ?? content.id
|
|
6
36
|
);
|
|
7
|
-
const document = {
|
|
8
|
-
route,
|
|
9
|
-
title
|
|
10
|
-
};
|
|
37
|
+
const document = { route, title };
|
|
11
38
|
const description = content.seo?.description ?? content.data["description"];
|
|
12
39
|
if (typeof description === "string" && description.length > 0) {
|
|
13
40
|
document.description = description;
|
|
@@ -16,13 +43,12 @@ function adaptContentItem(content) {
|
|
|
16
43
|
if (typeof canonical === "string" && canonical.length > 0) {
|
|
17
44
|
document.canonical = canonical;
|
|
18
45
|
}
|
|
19
|
-
|
|
20
|
-
|
|
46
|
+
const locale = content.locale || options.defaultLocale;
|
|
47
|
+
if (typeof locale === "string" && locale.length > 0) {
|
|
48
|
+
document.lang = locale;
|
|
21
49
|
}
|
|
22
50
|
const body = extractPortableText(content.data);
|
|
23
|
-
if (body !== null)
|
|
24
|
-
document.body = body;
|
|
25
|
-
}
|
|
51
|
+
if (body !== null) document.body = body;
|
|
26
52
|
return {
|
|
27
53
|
document,
|
|
28
54
|
meta: {
|
|
@@ -30,48 +56,226 @@ function adaptContentItem(content) {
|
|
|
30
56
|
collection: content.type,
|
|
31
57
|
slug: content.slug,
|
|
32
58
|
status: content.status,
|
|
33
|
-
title
|
|
59
|
+
title,
|
|
60
|
+
publicUrl: options.publicUrl ?? null
|
|
34
61
|
}
|
|
35
62
|
};
|
|
36
63
|
}
|
|
37
64
|
function deriveRoute(content) {
|
|
38
|
-
|
|
39
|
-
|
|
65
|
+
return `/${encodeURIComponent(content.type)}/${encodeURIComponent(content.id)}`;
|
|
66
|
+
}
|
|
67
|
+
function routeFromPublicUrl(publicUrl) {
|
|
68
|
+
try {
|
|
69
|
+
return new URL(publicUrl).pathname || "/";
|
|
70
|
+
} catch (cause) {
|
|
71
|
+
throw new Error(`EmDash returned an invalid public URL: ${publicUrl}`, {
|
|
72
|
+
cause
|
|
73
|
+
});
|
|
40
74
|
}
|
|
41
|
-
return `/${content.type}/${content.id}`;
|
|
42
75
|
}
|
|
43
76
|
function stringOrEmpty(value) {
|
|
44
77
|
return typeof value === "string" ? value : "";
|
|
45
78
|
}
|
|
46
79
|
function extractPortableText(data) {
|
|
47
|
-
const
|
|
48
|
-
for (const slug of candidates) {
|
|
80
|
+
for (const slug of ["body", "content"]) {
|
|
49
81
|
const value = data[slug];
|
|
50
|
-
if (isPortableTextArray(value))
|
|
51
|
-
|
|
82
|
+
if (isPortableTextArray(value)) return value;
|
|
83
|
+
if (isEmdashBlocksArray(value, slug)) return blocksToPortableText(value);
|
|
84
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
85
|
+
return [portableTextBlock(value.trim(), `${slug}-text`)];
|
|
52
86
|
}
|
|
53
87
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
88
|
+
const blocks = data["blocks"];
|
|
89
|
+
if (isPortableTextArray(blocks)) return blocks;
|
|
90
|
+
if (isEmdashBlocksArray(blocks, "blocks")) return blocksToPortableText(blocks);
|
|
91
|
+
for (const [field, value] of Object.entries(data)) {
|
|
92
|
+
if (field === "body" || field === "content" || field === "blocks") continue;
|
|
93
|
+
if (isPortableTextArray(value)) return value;
|
|
94
|
+
if (isEmdashBlocksArray(value, field)) return blocksToPortableText(value);
|
|
58
95
|
}
|
|
59
96
|
return null;
|
|
60
97
|
}
|
|
61
98
|
function isPortableTextArray(value) {
|
|
62
|
-
|
|
63
|
-
|
|
99
|
+
return Array.isArray(value) && value.length > 0 && value.every(
|
|
100
|
+
(item) => isRecord(item) && item["_type"] === "block" && Array.isArray(item["children"]) && item["children"].every(
|
|
101
|
+
(child) => isRecord(child) && typeof child["text"] === "string"
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
function isEmdashBlocksArray(value, field) {
|
|
106
|
+
if (!Array.isArray(value) || value.length === 0) return false;
|
|
107
|
+
return value.every(
|
|
108
|
+
(item) => isRecord(item) && typeof item["_type"] === "string" && typeof item["_key"] === "string" && (typeof item["_version"] === "number" || field === "blocks")
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
function blocksToPortableText(blocks) {
|
|
112
|
+
const paragraphs = [];
|
|
113
|
+
for (const block of blocks) {
|
|
114
|
+
const segments = [];
|
|
115
|
+
collectText(block, "", segments);
|
|
116
|
+
const text = segments.map((segment) => segment.trim()).filter(Boolean).join(" ");
|
|
117
|
+
if (text.length > 0) {
|
|
118
|
+
paragraphs.push(portableTextBlock(text, stringOrEmpty(block["_key"])));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return paragraphs;
|
|
122
|
+
}
|
|
123
|
+
function collectText(value, key, out) {
|
|
124
|
+
if (typeof value === "string") {
|
|
125
|
+
if (!isStructuralField(key) && value.trim().length > 0) out.push(value);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (Array.isArray(value)) {
|
|
129
|
+
for (const item of value) collectText(item, key, out);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!isRecord(value)) return;
|
|
133
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
134
|
+
if (isStructuralField(childKey)) continue;
|
|
135
|
+
collectText(childValue, childKey, out);
|
|
64
136
|
}
|
|
65
|
-
|
|
66
|
-
|
|
137
|
+
}
|
|
138
|
+
var STRUCTURAL_FIELDS = /* @__PURE__ */ new Set([
|
|
139
|
+
"_key",
|
|
140
|
+
"_ref",
|
|
141
|
+
"_type",
|
|
142
|
+
"_version",
|
|
143
|
+
"asset",
|
|
144
|
+
"align",
|
|
145
|
+
"alignment",
|
|
146
|
+
"autoplay",
|
|
147
|
+
"color",
|
|
148
|
+
"controls",
|
|
149
|
+
"focalpoint",
|
|
150
|
+
"height",
|
|
151
|
+
"href",
|
|
152
|
+
"id",
|
|
153
|
+
"layout",
|
|
154
|
+
"level",
|
|
155
|
+
"loop",
|
|
156
|
+
"markdefs",
|
|
157
|
+
"marks",
|
|
158
|
+
"mime_type",
|
|
159
|
+
"mimetype",
|
|
160
|
+
"position",
|
|
161
|
+
"rel",
|
|
162
|
+
"size",
|
|
163
|
+
"src",
|
|
164
|
+
"style",
|
|
165
|
+
"target",
|
|
166
|
+
"theme",
|
|
167
|
+
"tone",
|
|
168
|
+
"url",
|
|
169
|
+
"variant",
|
|
170
|
+
"width"
|
|
171
|
+
]);
|
|
172
|
+
function isStructuralField(key) {
|
|
173
|
+
const normalized = key.toLowerCase();
|
|
174
|
+
return normalized.startsWith("_") || STRUCTURAL_FIELDS.has(normalized);
|
|
175
|
+
}
|
|
176
|
+
function portableTextBlock(text, key) {
|
|
177
|
+
return {
|
|
178
|
+
_type: "block",
|
|
179
|
+
...key.length === 0 ? {} : { _key: key },
|
|
180
|
+
children: [
|
|
181
|
+
{
|
|
182
|
+
_type: "span",
|
|
183
|
+
...key.length === 0 ? {} : { _key: `${key}-text` },
|
|
184
|
+
text,
|
|
185
|
+
marks: []
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function isRecord(value) {
|
|
191
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/sidecar.ts
|
|
195
|
+
async function evaluateViaSidecar(http, config, documents) {
|
|
196
|
+
const body = {
|
|
197
|
+
documents
|
|
198
|
+
};
|
|
199
|
+
if (config.configJson !== void 0) {
|
|
200
|
+
body.configJson = config.configJson;
|
|
201
|
+
}
|
|
202
|
+
let response;
|
|
203
|
+
try {
|
|
204
|
+
response = await http.fetch(`${trimTrailingSlash(config.url)}/evaluate`, {
|
|
205
|
+
method: "POST",
|
|
206
|
+
headers: {
|
|
207
|
+
authorization: `Bearer ${config.authToken}`,
|
|
208
|
+
"content-type": "application/json"
|
|
209
|
+
},
|
|
210
|
+
body: JSON.stringify(body)
|
|
211
|
+
});
|
|
212
|
+
} catch (err) {
|
|
213
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
214
|
+
return { ok: false, reason: "network_error", detail };
|
|
215
|
+
}
|
|
216
|
+
if (response.status === 401 || response.status === 403) {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
reason: "auth_error",
|
|
220
|
+
detail: `sidecar rejected token (HTTP ${response.status})`
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
let detail = `sidecar HTTP ${response.status}`;
|
|
225
|
+
try {
|
|
226
|
+
detail = `${detail}: ${await response.text()}`;
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
return { ok: false, reason: "server_error", detail };
|
|
230
|
+
}
|
|
231
|
+
let raw;
|
|
232
|
+
try {
|
|
233
|
+
raw = await response.text();
|
|
234
|
+
} catch (err) {
|
|
235
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
236
|
+
return { ok: false, reason: "invalid_response", detail };
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const findings = JSON.parse(raw);
|
|
240
|
+
if (!Array.isArray(findings)) {
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
reason: "invalid_response",
|
|
244
|
+
detail: "sidecar returned non-array body"
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return { ok: true, findings };
|
|
248
|
+
} catch (err) {
|
|
249
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
250
|
+
return { ok: false, reason: "invalid_response", detail };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function trimTrailingSlash(url) {
|
|
254
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
67
255
|
}
|
|
68
256
|
|
|
69
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
|
+
}
|
|
70
266
|
var CONFIG_URL_KEY = "config:evaluator_url";
|
|
71
267
|
var CONFIG_TOKEN_KEY = "config:eval_token";
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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);
|
|
75
279
|
if (typeof url !== "string" || typeof token !== "string") {
|
|
76
280
|
return null;
|
|
77
281
|
}
|
|
@@ -80,11 +284,96 @@ async function readSidecarConfig(kv) {
|
|
|
80
284
|
}
|
|
81
285
|
return { url, token };
|
|
82
286
|
}
|
|
83
|
-
async function writeSidecarConfig(
|
|
84
|
-
await kv.set(CONFIG_URL_KEY, config.url);
|
|
85
|
-
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);
|
|
86
322
|
}
|
|
87
|
-
|
|
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]");
|
|
326
|
+
}
|
|
327
|
+
async function handleAfterSave(event, ctx) {
|
|
328
|
+
const runtime = await readSidecarConfig(ctx);
|
|
329
|
+
if (runtime === null) {
|
|
330
|
+
ctx.log?.warn?.(
|
|
331
|
+
"Aexeo afterSave skipped: configure the evaluator URL and token on the Setup page"
|
|
332
|
+
);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
await handleAfterSaveConfigured(
|
|
336
|
+
event,
|
|
337
|
+
ctx,
|
|
338
|
+
(documents) => evaluateViaSidecar(
|
|
339
|
+
ctx.http,
|
|
340
|
+
{ url: runtime.url, authToken: runtime.token },
|
|
341
|
+
documents
|
|
342
|
+
)
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilter) {
|
|
346
|
+
const adapted = await adaptContentItemWithContext(
|
|
347
|
+
event.content,
|
|
348
|
+
ctx.content,
|
|
349
|
+
ctx.site?.locale
|
|
350
|
+
);
|
|
351
|
+
const document = adapted.document;
|
|
352
|
+
const { kv, log } = ctx;
|
|
353
|
+
const stored = { document, meta: adapted.meta };
|
|
354
|
+
await persistDocument(ctx, stored);
|
|
355
|
+
const result = await evaluator([document]);
|
|
356
|
+
if (!result.ok) {
|
|
357
|
+
log?.error?.(`afterSave evaluator failure (${result.reason})`, {
|
|
358
|
+
detail: result.detail
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const pageFindings = result.findings.filter(
|
|
363
|
+
(finding) => finding.scope === "page"
|
|
364
|
+
);
|
|
365
|
+
const filtered = suppressionFilter === void 0 ? pageFindings : suppressionFilter.apply(
|
|
366
|
+
{
|
|
367
|
+
route: document.route,
|
|
368
|
+
collection: adapted.meta.collection,
|
|
369
|
+
status: adapted.meta.status
|
|
370
|
+
},
|
|
371
|
+
pageFindings
|
|
372
|
+
);
|
|
373
|
+
await kv.set(findingsKey(document.route), {
|
|
374
|
+
route: document.route,
|
|
375
|
+
findings: filtered
|
|
376
|
+
});
|
|
88
377
|
}
|
|
89
378
|
var DEFAULT_COLLECTIONS = ["posts", "pages"];
|
|
90
379
|
async function evaluateAndPersistAll(ctx, options) {
|
|
@@ -109,7 +398,11 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
109
398
|
...cursor === void 0 ? {} : { cursor }
|
|
110
399
|
});
|
|
111
400
|
for (const item of page.items) {
|
|
112
|
-
const adapted =
|
|
401
|
+
const adapted = await adaptContentItemWithContext(
|
|
402
|
+
item,
|
|
403
|
+
ctx.content,
|
|
404
|
+
ctx.site?.locale
|
|
405
|
+
);
|
|
113
406
|
documents.push(adapted.document);
|
|
114
407
|
documentRoutes.add(adapted.document.route);
|
|
115
408
|
adaptedByRoute.set(adapted.document.route, {
|
|
@@ -137,18 +430,18 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
137
430
|
summary.errors.push(`${result.reason}: ${result.detail}`);
|
|
138
431
|
return summary;
|
|
139
432
|
}
|
|
140
|
-
const
|
|
433
|
+
const findingsByRoute = /* @__PURE__ */ new Map();
|
|
141
434
|
for (const route of documentRoutes) {
|
|
142
|
-
|
|
435
|
+
findingsByRoute.set(route, []);
|
|
143
436
|
}
|
|
144
437
|
for (const finding of result.findings) {
|
|
145
438
|
const bucket = finding.scope === "page" ? finding.path : "*";
|
|
146
|
-
const list =
|
|
439
|
+
const list = findingsByRoute.get(bucket) ?? [];
|
|
147
440
|
list.push(finding);
|
|
148
|
-
|
|
441
|
+
findingsByRoute.set(bucket, list);
|
|
149
442
|
}
|
|
150
443
|
let totalAfterSuppression = 0;
|
|
151
|
-
for (const [route, findings] of
|
|
444
|
+
for (const [route, findings] of findingsByRoute) {
|
|
152
445
|
const stored = adaptedByRoute.get(route);
|
|
153
446
|
const context = {
|
|
154
447
|
route,
|
|
@@ -161,8 +454,57 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
161
454
|
totalAfterSuppression += filtered.length;
|
|
162
455
|
}
|
|
163
456
|
summary.totalFindings = totalAfterSuppression;
|
|
457
|
+
if (summary.errors.length === 0) {
|
|
458
|
+
await removeStaleDocuments(ctx, collections, documentRoutes);
|
|
459
|
+
}
|
|
164
460
|
return summary;
|
|
165
461
|
}
|
|
462
|
+
async function persistDocument(ctx, stored) {
|
|
463
|
+
const route = stored.document.route;
|
|
464
|
+
const entries = await ctx.kv.list("document:");
|
|
465
|
+
for (const entry of entries) {
|
|
466
|
+
const previous = toStoredDocument(entry.value);
|
|
467
|
+
if (previous === null || previous.document.route === route || previous.meta.id !== stored.meta.id || previous.meta.collection !== stored.meta.collection) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
await ctx.kv.delete(entry.key);
|
|
471
|
+
await ctx.kv.delete(findingsKey(previous.document.route));
|
|
472
|
+
}
|
|
473
|
+
await ctx.kv.set(documentKey(route), stored);
|
|
474
|
+
}
|
|
475
|
+
async function removeStaleDocuments(ctx, collections, currentRoutes) {
|
|
476
|
+
const scannedCollections = new Set(collections);
|
|
477
|
+
const entries = await ctx.kv.list("document:");
|
|
478
|
+
for (const entry of entries) {
|
|
479
|
+
const stored = toStoredDocument(entry.value);
|
|
480
|
+
if (stored === null || !scannedCollections.has(stored.meta.collection) || currentRoutes.has(stored.document.route)) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
await ctx.kv.delete(entry.key);
|
|
484
|
+
await ctx.kv.delete(findingsKey(stored.document.route));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function toStoredDocument(value) {
|
|
488
|
+
if (value === null || typeof value !== "object") return null;
|
|
489
|
+
if ("document" in value && "meta" in value && value.document !== null && typeof value.document === "object" && "route" in value.document && typeof value.document.route === "string" && value.meta !== null && typeof value.meta === "object") {
|
|
490
|
+
return value;
|
|
491
|
+
}
|
|
492
|
+
if ("route" in value && typeof value.route === "string") {
|
|
493
|
+
const document = value;
|
|
494
|
+
return {
|
|
495
|
+
document,
|
|
496
|
+
meta: {
|
|
497
|
+
id: document.route,
|
|
498
|
+
collection: "",
|
|
499
|
+
slug: null,
|
|
500
|
+
status: "",
|
|
501
|
+
title: document.title ?? "",
|
|
502
|
+
publicUrl: null
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
166
508
|
function findingsKey(route) {
|
|
167
509
|
const normalized = route === "" || route === "/" ? "/" : route;
|
|
168
510
|
return `findings:${normalized}`;
|
|
@@ -178,25 +520,8 @@ async function readAllStoredDocuments(kv) {
|
|
|
178
520
|
const entries = await kv.list("document:");
|
|
179
521
|
const out = [];
|
|
180
522
|
for (const entry of entries) {
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
183
|
-
if ("document" in value && "meta" in value) {
|
|
184
|
-
out.push(value);
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
if ("route" in value) {
|
|
188
|
-
const document = value;
|
|
189
|
-
out.push({
|
|
190
|
-
document,
|
|
191
|
-
meta: {
|
|
192
|
-
id: document.route,
|
|
193
|
-
collection: "",
|
|
194
|
-
slug: null,
|
|
195
|
-
status: "",
|
|
196
|
-
title: document.title ?? ""
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
}
|
|
523
|
+
const stored = toStoredDocument(entry.value);
|
|
524
|
+
if (stored !== null) out.push(stored);
|
|
200
525
|
}
|
|
201
526
|
return out;
|
|
202
527
|
}
|
|
@@ -208,367 +533,167 @@ async function readFindings(kv, route) {
|
|
|
208
533
|
return stored.findings;
|
|
209
534
|
}
|
|
210
535
|
|
|
211
|
-
// src/sidecar.ts
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (config.configJson !== void 0) {
|
|
217
|
-
body.configJson = config.configJson;
|
|
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." };
|
|
218
541
|
}
|
|
219
|
-
let
|
|
542
|
+
let parsed;
|
|
220
543
|
try {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
authorization: `Bearer ${config.authToken}`,
|
|
225
|
-
"content-type": "application/json"
|
|
226
|
-
},
|
|
227
|
-
body: JSON.stringify(body)
|
|
228
|
-
});
|
|
229
|
-
} catch (err) {
|
|
230
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
231
|
-
return { ok: false, reason: "network_error", detail };
|
|
544
|
+
parsed = new URL(url);
|
|
545
|
+
} catch {
|
|
546
|
+
return { ok: false, error: `Sidecar URL is not a valid URL: ${url}` };
|
|
232
547
|
}
|
|
233
|
-
if (
|
|
548
|
+
if (parsed.protocol !== "https:") {
|
|
234
549
|
return {
|
|
235
550
|
ok: false,
|
|
236
|
-
|
|
237
|
-
detail: `sidecar rejected token (HTTP ${response.status})`
|
|
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}`
|
|
238
552
|
};
|
|
239
553
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
return { ok: false, reason: "server_error", detail };
|
|
247
|
-
}
|
|
248
|
-
let raw;
|
|
249
|
-
try {
|
|
250
|
-
raw = await response.text();
|
|
251
|
-
} catch (err) {
|
|
252
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
253
|
-
return { ok: false, reason: "invalid_response", detail };
|
|
254
|
-
}
|
|
255
|
-
try {
|
|
256
|
-
const findings = JSON.parse(raw);
|
|
257
|
-
if (!Array.isArray(findings)) {
|
|
258
|
-
return {
|
|
259
|
-
ok: false,
|
|
260
|
-
reason: "invalid_response",
|
|
261
|
-
detail: "sidecar returned non-array body"
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
return { ok: true, findings };
|
|
265
|
-
} catch (err) {
|
|
266
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
267
|
-
return { ok: false, reason: "invalid_response", detail };
|
|
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
|
+
};
|
|
268
560
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// src/evaluator-sandbox.ts
|
|
275
|
-
async function evaluate(_documents) {
|
|
276
|
-
return [];
|
|
277
|
-
}
|
|
278
|
-
async function scoreSite(_documents) {
|
|
279
|
-
return {
|
|
280
|
-
overall_score: 0,
|
|
281
|
-
citation_readiness_score: 0,
|
|
282
|
-
truth_consistency_score: 0,
|
|
283
|
-
answer_pack_score: 0,
|
|
284
|
-
external_trust_alignment_score: null,
|
|
285
|
-
route_scores: [],
|
|
286
|
-
blockers: []
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
function errorFindings(findings) {
|
|
290
|
-
return findings.filter((finding) => finding.severity === "error");
|
|
291
|
-
}
|
|
292
|
-
function findingsByRoute(findings) {
|
|
293
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
294
|
-
for (const finding of findings) {
|
|
295
|
-
const bucket = grouped.get(finding.path);
|
|
296
|
-
if (bucket) {
|
|
297
|
-
bucket.push(finding);
|
|
298
|
-
} else {
|
|
299
|
-
grouped.set(finding.path, [finding]);
|
|
300
|
-
}
|
|
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
|
+
};
|
|
301
566
|
}
|
|
302
|
-
return
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// src/indexnow.ts
|
|
306
|
-
var INDEXNOW_ENDPOINT = "https://api.indexnow.org/indexnow";
|
|
307
|
-
async function submitIndexNow(config, urls) {
|
|
308
|
-
const host = hostOf(config.siteUrl);
|
|
309
|
-
if (host === null) {
|
|
310
|
-
return failure("siteUrl is not a valid URL", []);
|
|
311
|
-
}
|
|
312
|
-
const keyLocation = config.keyLocation ?? defaultKeyLocation(config);
|
|
313
|
-
const { submitted, rejected } = partitionByHost(urls, host);
|
|
314
|
-
if (submitted.length === 0) {
|
|
315
|
-
return failure("no submittable URLs after host check", rejected);
|
|
316
|
-
}
|
|
317
|
-
const payload = {
|
|
318
|
-
host,
|
|
319
|
-
key: config.key,
|
|
320
|
-
keyLocation,
|
|
321
|
-
urlList: submitted
|
|
322
|
-
};
|
|
323
|
-
const response = await fetch(INDEXNOW_ENDPOINT, {
|
|
324
|
-
method: "POST",
|
|
325
|
-
headers: { "content-type": "application/json" },
|
|
326
|
-
body: JSON.stringify(payload)
|
|
327
|
-
});
|
|
328
|
-
return {
|
|
329
|
-
ok: response.ok,
|
|
330
|
-
status: response.status,
|
|
331
|
-
endpoint: INDEXNOW_ENDPOINT,
|
|
332
|
-
submitted,
|
|
333
|
-
rejected
|
|
334
|
-
};
|
|
567
|
+
return { ok: true, url };
|
|
335
568
|
}
|
|
336
|
-
function
|
|
337
|
-
|
|
338
|
-
return
|
|
339
|
-
} catch {
|
|
340
|
-
return null;
|
|
569
|
+
function isBlockedHost(host) {
|
|
570
|
+
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
571
|
+
return true;
|
|
341
572
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const siteUrl = config.siteUrl.endsWith("/") ? config.siteUrl.slice(0, -1) : config.siteUrl;
|
|
345
|
-
return `${siteUrl}/${config.key}.txt`;
|
|
346
|
-
}
|
|
347
|
-
function partitionByHost(urls, expectedHost) {
|
|
348
|
-
const submitted = [];
|
|
349
|
-
const rejected = [];
|
|
350
|
-
for (const candidate of urls) {
|
|
351
|
-
const host = hostOf(candidate);
|
|
352
|
-
if (host === expectedHost) {
|
|
353
|
-
submitted.push(candidate);
|
|
354
|
-
} else {
|
|
355
|
-
rejected.push(candidate);
|
|
356
|
-
}
|
|
573
|
+
if (host.startsWith("[")) {
|
|
574
|
+
return isBlockedIpv6(host);
|
|
357
575
|
}
|
|
358
|
-
return
|
|
576
|
+
return isBlockedIpv4(host);
|
|
359
577
|
}
|
|
360
|
-
function
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
endpoint: INDEXNOW_ENDPOINT,
|
|
365
|
-
submitted: [],
|
|
366
|
-
rejected,
|
|
367
|
-
reason
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// src/mcp.ts
|
|
372
|
-
var checkTool = {
|
|
373
|
-
name: "aexeo.check",
|
|
374
|
-
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.",
|
|
375
|
-
inputSchema: {
|
|
376
|
-
type: "object",
|
|
377
|
-
properties: {
|
|
378
|
-
documents: {
|
|
379
|
-
type: "array",
|
|
380
|
-
description: "Array of EmdashDocument values to evaluate. Each entry carries route, title, optional description/canonical/lang, alternates, meta map, schema, and Portable Text body."
|
|
381
|
-
},
|
|
382
|
-
configJson: {
|
|
383
|
-
type: "string",
|
|
384
|
-
description: "Optional JSON-serialized Aexeo config. When absent the evaluator uses Config::default(), matching a cold CLI run."
|
|
385
|
-
}
|
|
386
|
-
},
|
|
387
|
-
required: ["documents"]
|
|
388
|
-
},
|
|
389
|
-
handler: async ({ documents, configJson }) => {
|
|
390
|
-
const options = configJson === void 0 ? {} : { configJson };
|
|
391
|
-
const findings = await evaluate(documents, options);
|
|
392
|
-
const errors = errorFindings(findings);
|
|
393
|
-
const grouped = findingsByRoute(findings);
|
|
394
|
-
const byRoute = {};
|
|
395
|
-
for (const [route, items] of grouped.entries()) {
|
|
396
|
-
byRoute[route] = items;
|
|
397
|
-
}
|
|
398
|
-
return {
|
|
399
|
-
findings,
|
|
400
|
-
errors,
|
|
401
|
-
byRoute,
|
|
402
|
-
totals: {
|
|
403
|
-
findings: findings.length,
|
|
404
|
-
errors: errors.length,
|
|
405
|
-
warnings: findings.length - errors.length
|
|
406
|
-
}
|
|
407
|
-
};
|
|
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;
|
|
408
582
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
siteUrl: { type: "string" },
|
|
421
|
-
key: { type: "string" },
|
|
422
|
-
keyLocation: { type: "string" }
|
|
423
|
-
},
|
|
424
|
-
required: ["siteUrl", "key"]
|
|
425
|
-
},
|
|
426
|
-
urls: {
|
|
427
|
-
type: "array",
|
|
428
|
-
items: { type: "string" },
|
|
429
|
-
description: "Absolute URLs to submit. Must share siteUrl's host."
|
|
430
|
-
}
|
|
431
|
-
},
|
|
432
|
-
required: ["config", "urls"]
|
|
433
|
-
},
|
|
434
|
-
handler: async ({ config, urls }) => submitIndexNow(config, urls)
|
|
435
|
-
};
|
|
436
|
-
var tools = [checkTool, indexNowSubmitTool];
|
|
437
|
-
|
|
438
|
-
// src/sandbox-entry.ts
|
|
439
|
-
function definePlugin(plugin) {
|
|
440
|
-
return plugin;
|
|
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;
|
|
441
594
|
}
|
|
442
|
-
function
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const raw = input;
|
|
447
|
-
if (raw.type === "page_load") {
|
|
448
|
-
return {
|
|
449
|
-
type: "page_load",
|
|
450
|
-
page: typeof raw.page === "string" && raw.page.length > 0 ? raw.page : "/findings"
|
|
451
|
-
};
|
|
595
|
+
function isBlockedIpv6(host) {
|
|
596
|
+
const inner = host.slice(1, -1).toLowerCase();
|
|
597
|
+
if (inner === "::" || inner === "::1") {
|
|
598
|
+
return true;
|
|
452
599
|
}
|
|
453
|
-
if (
|
|
454
|
-
return
|
|
455
|
-
type: "block_action",
|
|
456
|
-
action_id: typeof raw.action_id === "string" ? raw.action_id : "",
|
|
457
|
-
...typeof raw.block_id === "string" ? { block_id: raw.block_id } : {},
|
|
458
|
-
value: raw.value
|
|
459
|
-
};
|
|
600
|
+
if (inner.startsWith("::ffff:") || inner.startsWith("::7f")) {
|
|
601
|
+
return true;
|
|
460
602
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
action_id: typeof raw.action_id === "string" ? raw.action_id : "",
|
|
465
|
-
...typeof raw.block_id === "string" ? { block_id: raw.block_id } : {},
|
|
466
|
-
values: raw.values && typeof raw.values === "object" ? raw.values : {}
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
return { type: "page_load", page: "/findings" };
|
|
470
|
-
}
|
|
471
|
-
async function handleAdminRoute(input, ctx) {
|
|
472
|
-
const body = normalizeInteraction(input?.input);
|
|
473
|
-
ctx.log?.info?.(
|
|
474
|
-
`aexeo route: type=${body.type} page=${body.type === "page_load" ? body.page : ""}`
|
|
475
|
-
);
|
|
476
|
-
const dispatch = { body, kv: ctx.kv, ctx };
|
|
477
|
-
if (body.type === "page_load") {
|
|
478
|
-
return handlePageLoad(dispatch, body.page);
|
|
603
|
+
const firstHextet = Number.parseInt(inner.split(":")[0] ?? "", 16);
|
|
604
|
+
if (Number.isNaN(firstHextet)) {
|
|
605
|
+
return false;
|
|
479
606
|
}
|
|
480
|
-
if (
|
|
481
|
-
return
|
|
482
|
-
dispatch,
|
|
483
|
-
body.action_id,
|
|
484
|
-
body.value
|
|
485
|
-
);
|
|
607
|
+
if ((firstHextet & 65024) === 64512) {
|
|
608
|
+
return true;
|
|
486
609
|
}
|
|
487
|
-
if (
|
|
488
|
-
return
|
|
489
|
-
dispatch,
|
|
490
|
-
body.action_id,
|
|
491
|
-
body.values
|
|
492
|
-
);
|
|
610
|
+
if ((firstHextet & 65472) === 65152) {
|
|
611
|
+
return true;
|
|
493
612
|
}
|
|
494
|
-
return
|
|
613
|
+
return false;
|
|
495
614
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const picked = values["route_picker"];
|
|
502
|
-
if (typeof picked === "string" && picked.length > 0) {
|
|
503
|
-
return renderDocumentPanel(ctx, picked);
|
|
615
|
+
function hostMatchesAllowlist(host, allowedHosts) {
|
|
616
|
+
return allowedHosts.some((allowed) => {
|
|
617
|
+
const entry = allowed.trim().toLowerCase();
|
|
618
|
+
if (entry.length === 0) {
|
|
619
|
+
return false;
|
|
504
620
|
}
|
|
505
|
-
|
|
506
|
-
|
|
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
|
|
630
|
+
async function scoreSite(_documents) {
|
|
631
|
+
return {
|
|
632
|
+
overall_score: 0,
|
|
633
|
+
citation_readiness_score: 0,
|
|
634
|
+
truth_consistency_score: 0,
|
|
635
|
+
answer_pack_score: 0,
|
|
636
|
+
external_trust_alignment_score: null,
|
|
637
|
+
route_scores: [],
|
|
638
|
+
blockers: []
|
|
639
|
+
};
|
|
507
640
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if (normalized === "widget:aexeo-score") {
|
|
514
|
-
return renderScoreWidget(ctx);
|
|
515
|
-
}
|
|
516
|
-
if (normalized === "document") {
|
|
517
|
-
return renderDocumentPanel(ctx);
|
|
518
|
-
}
|
|
519
|
-
if (normalized === "setup") {
|
|
520
|
-
return renderSetupPage(ctx);
|
|
641
|
+
|
|
642
|
+
// src/findings-view.ts
|
|
643
|
+
function normalizeInteraction(input) {
|
|
644
|
+
if (!input || typeof input !== "object") {
|
|
645
|
+
return { type: "page_load", page: "/findings" };
|
|
521
646
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
647
|
+
const raw = input;
|
|
648
|
+
if (raw.type === "page_load") {
|
|
649
|
+
return {
|
|
650
|
+
type: "page_load",
|
|
651
|
+
page: typeof raw.page === "string" && raw.page.length > 0 ? raw.page : "/findings"
|
|
652
|
+
};
|
|
527
653
|
}
|
|
528
|
-
if (
|
|
529
|
-
return
|
|
654
|
+
if (raw.type === "block_action") {
|
|
655
|
+
return {
|
|
656
|
+
type: "block_action",
|
|
657
|
+
action_id: typeof raw.action_id === "string" ? raw.action_id : "",
|
|
658
|
+
...typeof raw.block_id === "string" ? { block_id: raw.block_id } : {},
|
|
659
|
+
value: raw.value
|
|
660
|
+
};
|
|
530
661
|
}
|
|
531
|
-
if (
|
|
532
|
-
return
|
|
662
|
+
if (raw.type === "form_submit") {
|
|
663
|
+
return {
|
|
664
|
+
type: "form_submit",
|
|
665
|
+
action_id: typeof raw.action_id === "string" ? raw.action_id : "",
|
|
666
|
+
...typeof raw.block_id === "string" ? { block_id: raw.block_id } : {},
|
|
667
|
+
values: raw.values && typeof raw.values === "object" ? raw.values : {}
|
|
668
|
+
};
|
|
533
669
|
}
|
|
534
|
-
return
|
|
670
|
+
return { type: "page_load", page: "/findings" };
|
|
535
671
|
}
|
|
536
|
-
async function handleRefresh(
|
|
537
|
-
const sandboxEvaluator = async (documents) => {
|
|
538
|
-
const runtime = await readSidecarConfig(ctx.kv);
|
|
539
|
-
if (runtime === null) {
|
|
540
|
-
return {
|
|
541
|
-
ok: false,
|
|
542
|
-
reason: "config_missing",
|
|
543
|
-
detail: "sidecar not configured \u2014 open the Aexeo Setup page and enter your evaluator URL and token"
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
return evaluateViaSidecar(
|
|
547
|
-
ctx.ctx.http,
|
|
548
|
-
{ url: runtime.url, authToken: runtime.token },
|
|
549
|
-
documents
|
|
550
|
-
);
|
|
551
|
-
};
|
|
672
|
+
async function handleRefresh(options) {
|
|
552
673
|
let summary;
|
|
553
674
|
try {
|
|
554
|
-
summary = await evaluateAndPersistAll(
|
|
555
|
-
|
|
675
|
+
summary = await evaluateAndPersistAll(options.ctx, {
|
|
676
|
+
collections: options.collections,
|
|
677
|
+
evaluator: options.evaluator,
|
|
678
|
+
// Always forwarded. See RefreshOptions.suppressionFilter.
|
|
679
|
+
suppressionFilter: options.suppressionFilter
|
|
556
680
|
});
|
|
557
681
|
} catch (err) {
|
|
558
682
|
const detail = err instanceof Error ? err.message : String(err);
|
|
683
|
+
const message = `Refresh failed: ${detail}`;
|
|
559
684
|
return {
|
|
560
685
|
blocks: [
|
|
561
686
|
{ type: "header", text: "SEO findings" },
|
|
562
687
|
{
|
|
563
688
|
type: "banner",
|
|
564
|
-
title:
|
|
565
|
-
variant:
|
|
689
|
+
title: message,
|
|
690
|
+
variant: options.tuning.refreshFailureVariant
|
|
566
691
|
}
|
|
567
692
|
],
|
|
568
|
-
toast: { message
|
|
693
|
+
toast: { message, type: "error" }
|
|
569
694
|
};
|
|
570
695
|
}
|
|
571
|
-
const refreshed = await renderFindingsPage(ctx);
|
|
696
|
+
const refreshed = await renderFindingsPage(options.ctx.kv, options.tuning);
|
|
572
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`;
|
|
573
698
|
if (summary.errors.length > 0) {
|
|
574
699
|
refreshed.blocks.unshift({
|
|
@@ -585,10 +710,43 @@ async function handleRefresh(ctx) {
|
|
|
585
710
|
}
|
|
586
711
|
};
|
|
587
712
|
}
|
|
588
|
-
async function
|
|
589
|
-
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);
|
|
590
746
|
const errors = findings.filter((finding) => finding.severity === "error");
|
|
591
|
-
const warnings = findings.filter(
|
|
747
|
+
const warnings = findings.filter(
|
|
748
|
+
(finding) => finding.severity === "warning"
|
|
749
|
+
);
|
|
592
750
|
const sorted = [...findings].sort(severityFirst);
|
|
593
751
|
const routes = uniqueRoutes(findings);
|
|
594
752
|
const blocks = [
|
|
@@ -601,11 +759,9 @@ async function renderFindingsPage(ctx) {
|
|
|
601
759
|
{
|
|
602
760
|
type: "actions",
|
|
603
761
|
elements: [
|
|
604
|
-
// Refresh
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
// Pressing this lists all content via the live in-request bridge,
|
|
608
|
-
// calls the sidecar /evaluate, and writes findings back to KV.
|
|
762
|
+
// Refresh evaluates every configured entry. Pressing it lists
|
|
763
|
+
// content via the live in-request bridge, evaluates it, and
|
|
764
|
+
// writes findings back to KV (with suppressions applied).
|
|
609
765
|
{
|
|
610
766
|
type: "button",
|
|
611
767
|
label: "Refresh",
|
|
@@ -623,7 +779,7 @@ async function renderFindingsPage(ctx) {
|
|
|
623
779
|
},
|
|
624
780
|
sorted.length === 0 ? {
|
|
625
781
|
type: "context",
|
|
626
|
-
text:
|
|
782
|
+
text: tuning.emptyFindingsText
|
|
627
783
|
} : findingsTable(sorted)
|
|
628
784
|
];
|
|
629
785
|
if (routes.length > 0) {
|
|
@@ -642,37 +798,6 @@ async function renderFindingsPage(ctx) {
|
|
|
642
798
|
}
|
|
643
799
|
return { blocks };
|
|
644
800
|
}
|
|
645
|
-
async function readAllFindings(kv) {
|
|
646
|
-
const entries = await kv.list(
|
|
647
|
-
"findings:"
|
|
648
|
-
);
|
|
649
|
-
const out = [];
|
|
650
|
-
for (const entry of entries) {
|
|
651
|
-
if (entry.value === null) {
|
|
652
|
-
continue;
|
|
653
|
-
}
|
|
654
|
-
const route = entry.key.replace(/^findings:/, "");
|
|
655
|
-
for (const finding of entry.value.findings) {
|
|
656
|
-
out.push({ ...finding, document_route: route });
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
return out;
|
|
660
|
-
}
|
|
661
|
-
function uniqueRoutes(rows) {
|
|
662
|
-
const routes = /* @__PURE__ */ new Set();
|
|
663
|
-
for (const row of rows) {
|
|
664
|
-
routes.add(row.document_route);
|
|
665
|
-
}
|
|
666
|
-
return [...routes].sort();
|
|
667
|
-
}
|
|
668
|
-
function severityFirst(a, b) {
|
|
669
|
-
const rank = (severity) => severity === "error" ? 0 : 1;
|
|
670
|
-
const diff = rank(a.severity) - rank(b.severity);
|
|
671
|
-
if (diff !== 0) {
|
|
672
|
-
return diff;
|
|
673
|
-
}
|
|
674
|
-
return a.rule_id.localeCompare(b.rule_id);
|
|
675
|
-
}
|
|
676
801
|
function findingsTable(rows) {
|
|
677
802
|
return {
|
|
678
803
|
type: "table",
|
|
@@ -690,40 +815,31 @@ function findingsTable(rows) {
|
|
|
690
815
|
}))
|
|
691
816
|
};
|
|
692
817
|
}
|
|
693
|
-
async function renderScoreWidget(
|
|
694
|
-
const documents = await readAllDocuments(
|
|
818
|
+
async function renderScoreWidget(kv, tuning) {
|
|
819
|
+
const documents = await readAllDocuments(kv);
|
|
695
820
|
if (documents.length === 0) {
|
|
696
821
|
return {
|
|
697
822
|
blocks: [
|
|
698
823
|
{ type: "header", text: "SEO score" },
|
|
699
824
|
{
|
|
700
825
|
type: "context",
|
|
701
|
-
text:
|
|
826
|
+
text: tuning.emptyScoreText
|
|
702
827
|
}
|
|
703
828
|
]
|
|
704
829
|
};
|
|
705
830
|
}
|
|
706
|
-
const score = await
|
|
831
|
+
const score = await tuning.score(documents, kv);
|
|
707
832
|
const blocks = [
|
|
708
833
|
{
|
|
709
834
|
type: "stats",
|
|
710
835
|
items: [
|
|
836
|
+
{ label: "Overall", value: `${score.overall_score}` },
|
|
837
|
+
{ label: "Citation", value: `${score.citation_readiness_score}` },
|
|
711
838
|
{
|
|
712
|
-
label:
|
|
713
|
-
value: `${score.overall_score}`
|
|
714
|
-
},
|
|
715
|
-
{
|
|
716
|
-
label: "Citation",
|
|
717
|
-
value: `${score.citation_readiness_score}`
|
|
718
|
-
},
|
|
719
|
-
{
|
|
720
|
-
label: "Truth",
|
|
839
|
+
label: tuning.truthLabel(score),
|
|
721
840
|
value: `${score.truth_consistency_score}`
|
|
722
841
|
},
|
|
723
|
-
{
|
|
724
|
-
label: "Answers",
|
|
725
|
-
value: `${score.answer_pack_score}`
|
|
726
|
-
}
|
|
842
|
+
{ label: "Answers", value: `${score.answer_pack_score}` }
|
|
727
843
|
]
|
|
728
844
|
}
|
|
729
845
|
];
|
|
@@ -749,7 +865,7 @@ function topBlockersLine(score) {
|
|
|
749
865
|
}
|
|
750
866
|
return `Top blockers: ${top.join(" \u2022 ")}`;
|
|
751
867
|
}
|
|
752
|
-
async function renderDocumentPanel(
|
|
868
|
+
async function renderDocumentPanel(kv, route) {
|
|
753
869
|
if (route === void 0) {
|
|
754
870
|
return {
|
|
755
871
|
blocks: [
|
|
@@ -761,9 +877,11 @@ async function renderDocumentPanel(ctx, route) {
|
|
|
761
877
|
]
|
|
762
878
|
};
|
|
763
879
|
}
|
|
764
|
-
const findings = await readFindings(
|
|
880
|
+
const findings = await readFindings(kv, route);
|
|
765
881
|
const errors = findings.filter((finding) => finding.severity === "error");
|
|
766
|
-
const warnings = findings.filter(
|
|
882
|
+
const warnings = findings.filter(
|
|
883
|
+
(finding) => finding.severity === "warning"
|
|
884
|
+
);
|
|
767
885
|
const sorted = [...findings].sort(severityFirst);
|
|
768
886
|
return {
|
|
769
887
|
blocks: [
|
|
@@ -796,13 +914,185 @@ function documentFindingsTable(findings) {
|
|
|
796
914
|
}))
|
|
797
915
|
};
|
|
798
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
|
+
}
|
|
799
1089
|
async function renderSetupPage(ctx, options = {}) {
|
|
800
|
-
const existing = await readSidecarConfig(ctx.
|
|
1090
|
+
const existing = await readSidecarConfig(ctx.ctx);
|
|
801
1091
|
const blocks = [
|
|
802
1092
|
{ type: "header", text: "Aexeo setup" },
|
|
803
1093
|
{
|
|
804
1094
|
type: "context",
|
|
805
|
-
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."
|
|
806
1096
|
}
|
|
807
1097
|
];
|
|
808
1098
|
if (options.bannerError !== void 0) {
|
|
@@ -850,39 +1140,23 @@ async function renderSetupPage(ctx, options = {}) {
|
|
|
850
1140
|
);
|
|
851
1141
|
return { blocks };
|
|
852
1142
|
}
|
|
1143
|
+
function runtimeAllowedHosts() {
|
|
1144
|
+
const env = typeof process === "undefined" ? void 0 : process.env;
|
|
1145
|
+
return buildAllowedHosts(env?.["AEXEO_EVALUATOR_HOST"] ?? null);
|
|
1146
|
+
}
|
|
853
1147
|
async function handleSetupSubmit(ctx, values) {
|
|
854
1148
|
const rawUrl = values["evaluator_url"];
|
|
855
1149
|
const rawToken = values["eval_token"];
|
|
856
1150
|
const url = typeof rawUrl === "string" ? rawUrl.trim() : "";
|
|
857
1151
|
const token = typeof rawToken === "string" ? rawToken.trim() : "";
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
bannerError: "Sidecar URL is required.",
|
|
861
|
-
initialUrl: url
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
let parsed;
|
|
865
|
-
try {
|
|
866
|
-
parsed = new URL(url);
|
|
867
|
-
} catch {
|
|
868
|
-
return renderSetupPage(ctx, {
|
|
869
|
-
bannerError: `Sidecar URL is not a valid URL: ${url}`,
|
|
870
|
-
initialUrl: url
|
|
871
|
-
});
|
|
872
|
-
}
|
|
873
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
1152
|
+
const validated = validateSidecarUrl(url, runtimeAllowedHosts());
|
|
1153
|
+
if (!validated.ok) {
|
|
874
1154
|
return renderSetupPage(ctx, {
|
|
875
|
-
bannerError:
|
|
1155
|
+
bannerError: validated.error,
|
|
876
1156
|
initialUrl: url
|
|
877
1157
|
});
|
|
878
1158
|
}
|
|
879
|
-
|
|
880
|
-
return renderSetupPage(ctx, {
|
|
881
|
-
bannerError: "emdash's sandbox blocks fetches to localhost (anti-SSRF). Use the deployed *.workers.dev URL.",
|
|
882
|
-
initialUrl: url
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
const existing = await readSidecarConfig(ctx.kv);
|
|
1159
|
+
const existing = await readSidecarConfig(ctx.ctx);
|
|
886
1160
|
let resolvedToken;
|
|
887
1161
|
if (token.length > 0) {
|
|
888
1162
|
resolvedToken = token;
|
|
@@ -891,34 +1165,25 @@ async function handleSetupSubmit(ctx, values) {
|
|
|
891
1165
|
} else {
|
|
892
1166
|
return renderSetupPage(ctx, {
|
|
893
1167
|
bannerError: "EVAL_TOKEN is required on first setup.",
|
|
894
|
-
initialUrl: url
|
|
1168
|
+
initialUrl: validated.url
|
|
895
1169
|
});
|
|
896
1170
|
}
|
|
897
|
-
const next = {
|
|
898
|
-
|
|
1171
|
+
const next = {
|
|
1172
|
+
url: validated.url,
|
|
1173
|
+
token: resolvedToken
|
|
1174
|
+
};
|
|
1175
|
+
await writeSidecarConfig(ctx.ctx, next);
|
|
899
1176
|
return renderSetupPage(ctx, {
|
|
900
1177
|
bannerSuccess: "Configuration saved. Click Refresh on the findings page to evaluate."
|
|
901
1178
|
});
|
|
902
1179
|
}
|
|
903
|
-
function notFound(page) {
|
|
904
|
-
return {
|
|
905
|
-
blocks: [
|
|
906
|
-
{ type: "header", text: "Not found" },
|
|
907
|
-
{ type: "context", text: `unknown page: ${page}` }
|
|
908
|
-
]
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
1180
|
var sandbox_entry_default = definePlugin({
|
|
912
1181
|
hooks: {
|
|
913
1182
|
"content:afterSave": handleAfterSave
|
|
914
1183
|
},
|
|
915
1184
|
routes: {
|
|
916
1185
|
admin: { handler: handleAdminRoute }
|
|
917
|
-
}
|
|
918
|
-
// emdash's MCP server picks tools up from this field. The exact
|
|
919
|
-
// host-side spec for plugin-contributed tools is still being mapped;
|
|
920
|
-
// the field is harmless if unrecognized.
|
|
921
|
-
mcpTools: tools
|
|
1186
|
+
}
|
|
922
1187
|
});
|
|
923
1188
|
export {
|
|
924
1189
|
sandbox_entry_default as default
|