Skip to content

Commit c03206c

Browse files
Address PR review feedback on AI Web API validator
- Fix handlesTokenInline citation-parser detection: match the escaped- bracket regex form (/\[\[(\d+)\]\]\(/) the skill actually ships, plus a literal [[N]]( token fallback, instead of a regex that never matched. - Make header/$select/content-type checks project-wide instead of per-file, so integrations that centralize header and URL construction in a shared helper no longer false-fail (matches validate-webapi-integration.js). - Clarify CSRF rationale: the token is required on these POST requests regardless of mutation semantics; the endpoints are read-only. - Fix prompt-length error guidance: the Data Summarization body accepts no prompt text (only InstructionIdentifier/RecommendationConfig), so direct makers to shorten the site-setting prompt value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d2141a8 commit c03206c

1 file changed

Lines changed: 65 additions & 56 deletions

File tree

plugins/power-pages/skills/add-ai-webapi/scripts/validate-ai-webapi.js

Lines changed: 65 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,22 @@
77
// - a search-summary service that POSTs to /_api/search/v1.0/summary
88
// - a data-summarization service that POSTs to /_api/summarization/data/v1.0/
99
//
10-
// Blocking checks (these are documented or structural and break the API at runtime):
11-
// - Every summarization request must attach the __RequestVerificationToken header.
12-
// The Data Summarization docs explicitly require a CSRF token; omitting it causes
13-
// server-side token validation to fail even though the operation is semantically read-only.
14-
// - Every data-summarization call must include $select — Power Pages Web API never
15-
// allows wildcard columns, and the Microsoft sample URL has $select.
16-
// - Every data-summarization call must set OData-MaxVersion: 4.0 and OData-Version: 4.0.
17-
// The endpoint inherits the Power Pages Web API rules and rejects requests without the
18-
// OData version headers.
19-
// - Every Search Summary call must use Content-Type: application/x-www-form-urlencoded.
20-
// The endpoint rejects application/json with a 400 — this is the most common
21-
// copy-paste failure when the data endpoint's headers leak into the search call.
10+
// Blocking checks (these are documented or structural and break the API at runtime). All are
11+
// project-wide: the required token must appear somewhere under src/, not necessarily in the same
12+
// file as the endpoint URL — a correct integration commonly centralizes header and URL
13+
// construction in a shared helper (the same pattern validate-webapi-integration.js accepts for
14+
// powerPagesApi.ts), so a per-file requirement would false-fail those projects.
15+
// - The integration must attach the __RequestVerificationToken header. The Data Summarization
16+
// docs require a CSRF token on these POST requests; omitting it produces a token-validation
17+
// failure. (These endpoints are semantically read-only — they never mutate Dataverse — but the
18+
// runtime enforces CSRF on POST regardless of mutation semantics, so the token is still required.)
19+
// - Data summarization must include $select — Power Pages Web API never allows wildcard columns,
20+
// and the Microsoft sample URL has $select.
21+
// - Data summarization must set OData-MaxVersion: 4.0 and OData-Version: 4.0. The endpoint
22+
// inherits the Power Pages Web API rules and rejects requests without the OData version headers.
23+
// - Search Summary must use Content-Type: application/x-www-form-urlencoded. The endpoint rejects
24+
// application/json with a 400 — this is the most common copy-paste failure when the data
25+
// endpoint's headers leak into the search call.
2226
//
2327
// Advisory only (missing prints a warning but does not block):
2428
// - X-Requested-With: XMLHttpRequest — matches shell.ajaxSafePost's default behaviour
@@ -94,57 +98,62 @@ runValidation((cwd) => {
9498
const errors = [];
9599
const warnings = [];
96100

97-
for (const hit of hits) {
98-
const rel = path.relative(projectRoot, hit.file);
99-
if (!hit.content.includes('__RequestVerificationToken')) {
100-
errors.push(`${rel}: summarization request missing __RequestVerificationToken header (CSRF token is required — fetch it from /_layout/tokenhtml)`);
101+
// Header and URL-token checks are project-wide, not per-file. A correct integration commonly
102+
// centralizes header construction and URL building in a shared helper (the same pattern
103+
// validate-webapi-integration.js accepts for powerPagesApi.ts), so requiring every token to
104+
// appear in the same file as the endpoint URL would false-fail those projects. We read the
105+
// whole src/ tree once and block only when a required token is absent project-wide.
106+
const allContent = sourceFiles
107+
.map((f) => {
108+
try {
109+
return fs.readFileSync(f, 'utf8');
110+
} catch {
111+
return '';
112+
}
113+
})
114+
.join('\n');
115+
116+
const projectHasData = hits.some((h) => h.hasData);
117+
const projectHasSearchSummary = hits.some((h) => h.hasSearch);
118+
119+
if (!allContent.includes('__RequestVerificationToken')) {
120+
errors.push('summarization integration is missing the __RequestVerificationToken header in every source file (CSRF token is required on these POST requests — fetch it from /_layout/tokenhtml)');
121+
}
122+
if (!allContent.includes('X-Requested-With')) {
123+
warnings.push('summarization integration does not set the X-Requested-With: XMLHttpRequest header in any source file (not strictly required by the docs, but matches shell.ajaxSafePost behaviour used by the Microsoft case-page snippet)');
124+
}
125+
if (projectHasData) {
126+
if (!/\$select=/.test(allContent)) {
127+
errors.push('data summarization integration is missing $select in every source file — Power Pages Web API requires explicit column lists, never wildcards');
101128
}
102-
if (!hit.content.includes('X-Requested-With')) {
103-
warnings.push(`${rel}: summarization request missing X-Requested-With: XMLHttpRequest header (not strictly required by the docs, but matches shell.ajaxSafePost behaviour used by the Microsoft case-page snippet)`);
129+
// OData 4.0 headers are mandatory on the data-summarization endpoint — it inherits
130+
// the Power Pages Web API rules and rejects requests without them.
131+
if (!allContent.includes('OData-MaxVersion')) {
132+
errors.push('data summarization integration is missing the OData-MaxVersion: 4.0 header in every source file — the Power Pages Web API rejects requests without it');
104133
}
105-
if (hit.hasData && !/\$select=/.test(hit.content)) {
106-
errors.push(`${rel}: data summarization call missing $select — Power Pages Web API requires explicit column lists, never wildcards`);
134+
if (!allContent.includes('OData-Version')) {
135+
errors.push('data summarization integration is missing the OData-Version: 4.0 header in every source file — the Power Pages Web API rejects requests without it');
107136
}
108-
if (hit.hasData) {
109-
// OData 4.0 headers are mandatory on the data-summarization endpoint — it inherits
110-
// the Power Pages Web API rules and rejects requests without them.
111-
if (!hit.content.includes('OData-MaxVersion')) {
112-
errors.push(`${rel}: data summarization call missing OData-MaxVersion: 4.0 header — the Power Pages Web API rejects requests without it`);
113-
}
114-
if (!hit.content.includes('OData-Version')) {
115-
errors.push(`${rel}: data summarization call missing OData-Version: 4.0 header — the Power Pages Web API rejects requests without it`);
116-
}
117-
}
118-
if (hit.hasSearch) {
119-
// Search Summary requires application/x-www-form-urlencoded. Sending application/json
120-
// (the most common copy-paste failure from the data endpoint) returns 400. We accept
121-
// the file as long as the form-urlencoded content type appears somewhere in it —
122-
// a single file can legitimately contain both Search and Data Summarization fetches.
123-
if (!hit.content.includes('application/x-www-form-urlencoded')) {
124-
errors.push(`${rel}: Search Summary call missing Content-Type: application/x-www-form-urlencoded — sending application/json returns 400 (this is the #1 way to break /_api/search/v1.0/summary)`);
125-
}
137+
}
138+
if (projectHasSearchSummary) {
139+
// Search Summary requires application/x-www-form-urlencoded. Sending application/json
140+
// (the most common copy-paste failure from the data endpoint) returns 400.
141+
if (!allContent.includes('application/x-www-form-urlencoded')) {
142+
errors.push('Search Summary integration is missing Content-Type: application/x-www-form-urlencoded in every source file — sending application/json returns 400 (this is the #1 way to break /_api/search/v1.0/summary)');
126143
}
127144
}
128145

129-
// Project-wide checks for Search Summary UI rendering. These look across all source files
130-
// because the parser/rewrite typically lives in a UI component (or a shared util), not the
131-
// file containing the fetch call. We only run them when the project actually calls
132-
// /_api/search/v1.0/summary somewhere.
133-
const projectHasSearchSummary = hits.some((h) => h.hasSearch);
146+
// Project-wide checks for Search Summary UI rendering. The parser/rewrite typically lives in a
147+
// UI component (or a shared util), not the file containing the fetch call. We only run them when
148+
// the project actually calls /_api/search/v1.0/summary somewhere.
134149
if (projectHasSearchSummary) {
135-
const allContent = sourceFiles
136-
.map((f) => {
137-
try {
138-
return fs.readFileSync(f, 'utf8');
139-
} catch {
140-
return '';
141-
}
142-
})
143-
.join('\n');
144-
145-
// [[N]](url) parser: either by helper name or by a literal pattern that handles the token.
150+
// [[N]](url) parser: either by helper name or by a pattern that handles the token.
151+
// The documented parser is a regex literal `/\[\[(\d+)\]\]\(([^)]+)\)/`, so the source
152+
// text contains the escaped-bracket characters `\[\[ ... \]\]\(`. Match that form (any
153+
// capture-group content between the escaped brackets), or a literal `[[N]](` token.
146154
const usesParserHelper = allContent.includes('parseSummaryWithCitations');
147-
const handlesTokenInline = /\[\[\\?d\+\\?\]\]\\?\(/.test(allContent) || /\[\[\d+\]\]\(/.test(allContent);
155+
const handlesTokenInline =
156+
/\\\[\\\[.*?\\\]\\\]\\\(/.test(allContent) || /\[\[\d+\]\]\(/.test(allContent);
148157
if (!usesParserHelper && !handlesTokenInline) {
149158
warnings.push(
150159
'Search Summary is integrated but no source file references parseSummaryWithCitations or a [[N]](url) parsing pattern — Summary will render as raw markdown unless a parser is wired in.'
@@ -248,7 +257,7 @@ runValidation((cwd) => {
248257
const charCount = promptValue.length;
249258
if (charCount > 2000) {
250259
errors.push(
251-
`${entry}: prompt value is ${charCount} characters, exceeding the supported maximum of 2000. Shorten the site-setting prompt value itself by removing repeated boilerplate and reducing or omitting inline examples in the prompt text.`
260+
`${entry}: prompt value is ${charCount} characters, exceeding the supported maximum of 2000. Shorten this site-setting prompt — condense or drop inline examples within the prompt text. The Data Summarization request body does not accept prompt text (only InstructionIdentifier / RecommendationConfig), so the full instruction must fit in this site-setting value.`
252261
);
253262
} else if (charCount > 1000) {
254263
warnings.push(

0 commit comments

Comments
 (0)