|
7 | 7 | // - a search-summary service that POSTs to /_api/search/v1.0/summary |
8 | 8 | // - a data-summarization service that POSTs to /_api/summarization/data/v1.0/ |
9 | 9 | // |
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. |
22 | 26 | // |
23 | 27 | // Advisory only (missing prints a warning but does not block): |
24 | 28 | // - X-Requested-With: XMLHttpRequest — matches shell.ajaxSafePost's default behaviour |
@@ -94,57 +98,62 @@ runValidation((cwd) => { |
94 | 98 | const errors = []; |
95 | 99 | const warnings = []; |
96 | 100 |
|
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'); |
101 | 128 | } |
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'); |
104 | 133 | } |
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'); |
107 | 136 | } |
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)'); |
126 | 143 | } |
127 | 144 | } |
128 | 145 |
|
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. |
134 | 149 | 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. |
146 | 154 | const usesParserHelper = allContent.includes('parseSummaryWithCitations'); |
147 | | - const handlesTokenInline = /\[\[\\?d\+\\?\]\]\\?\(/.test(allContent) || /\[\[\d+\]\]\(/.test(allContent); |
| 155 | + const handlesTokenInline = |
| 156 | + /\\\[\\\[.*?\\\]\\\]\\\(/.test(allContent) || /\[\[\d+\]\]\(/.test(allContent); |
148 | 157 | if (!usesParserHelper && !handlesTokenInline) { |
149 | 158 | warnings.push( |
150 | 159 | '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) => { |
248 | 257 | const charCount = promptValue.length; |
249 | 258 | if (charCount > 2000) { |
250 | 259 | 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.` |
252 | 261 | ); |
253 | 262 | } else if (charCount > 1000) { |
254 | 263 | warnings.push( |
|
0 commit comments