Skip to content

Commit 24e209f

Browse files
priyanshu92claude
andcommitted
Address Copilot review feedback on server logic skill
- Fix early approve() calls to return so execution terminates - Add fetch to disallowed browser APIs list - Scope web role GUID validation to adx_serverlogic_adx_webrole section - Strip YAML quotes before validating UUIDs and name fields - Require name field in serverlogic.yml (fail if missing) - Detect disallowed top-level functions outside the allowlist - Expand try/catch detection to full function body instead of 100-char window - Fix console.* error message to match the actual regex check - Surface filesystem errors in findServerLogicDirs instead of swallowing - Add path traversal guard on --name in create-serverlogic-metadata.js - Fix phase cross-reference (Phase 7 -> Phase 8) in SKILL.md - Renumber Phase 8 subsections (8.5/8.6 -> 8.3/8.4) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent adc1db3 commit 24e209f

3 files changed

Lines changed: 81 additions & 23 deletions

File tree

plugins/power-pages/skills/integrate-serverlogic/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ Use the **Explore agent** (via `Task` tool with `agent_type: "explore"`) to anal
9191
9292
From the Explore agent's findings, note:
9393
- **Existing server logic files** — what's already implemented, and which ones are candidates for reuse or extension
94-
- **Frontend calling patterns** — how the site makes API calls (match this pattern in Phase 7)
94+
- **Frontend calling patterns** — how the site makes API calls (match this pattern in Phase 8)
9595
- **Existing service/utility files** — reuse these when adding client-side integration
9696
- **Gaps** — frontend code that references server logic endpoints that don't exist yet
9797

@@ -543,7 +543,7 @@ Following the reference:
543543
- Replace placeholder data, mock handlers, or temporary actions when they are meant to be backed by the new server logic endpoints
544544
- Add or preserve loading, success, empty, and error states so the UI behaves like a finished feature
545545

546-
### 8.5 Ask User About Integration Scope
546+
### 8.3 Ask User About Integration Scope
547547

548548
Use `AskUserQuestion`:
549549

@@ -553,7 +553,7 @@ Use `AskUserQuestion`:
553553

554554
**If "No"**: Skip to Phase 9, but provide the API URL and a code snippet the user can copy.
555555

556-
### 8.6 Git Commit
556+
### 8.4 Git Commit
557557

558558
If frontend integration code was created:
559559

plugins/power-pages/skills/integrate-serverlogic/scripts/create-serverlogic-metadata.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ if (!projectRoot || !endpointName || !displayName || !description || !webRoleIds
3535
process.exit(1);
3636
}
3737

38+
// Validate endpointName is a safe slug (no path separators or traversal)
39+
if (!/^[a-zA-Z0-9_-]+$/.test(endpointName)) {
40+
console.error(`Error: --name must be a safe slug (alphanumeric, hyphens, underscores only). Got: "${endpointName}"`);
41+
process.exit(1);
42+
}
43+
3844
const webRoleIds = webRoleIdsRaw.split(',').map(id => id.trim()).filter(Boolean);
3945
if (webRoleIds.length === 0) {
4046
console.error('Error: --webRoleIds must contain at least one UUID');

plugins/power-pages/skills/integrate-serverlogic/scripts/validate-serverlogic.js

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@ const path = require('path');
99
const { approve, block, runValidation, findProjectRoot, UUID_REGEX } = require('../../../scripts/lib/validation-helpers');
1010

1111
const ALLOWED_FUNCTIONS = ['get', 'post', 'put', 'patch', 'del'];
12-
const BROWSER_APIS = ['XMLHttpRequest', 'document\\.', 'window\\.', 'setTimeout', 'setInterval', 'navigator\\.'];
12+
const BROWSER_APIS = ['XMLHttpRequest', 'document\\.', 'window\\.', 'setTimeout', 'setInterval', 'navigator\\.', 'fetch'];
1313

1414
runValidation((cwd) => {
1515
const projectRoot = findProjectRoot(cwd);
16-
if (!projectRoot) approve(); // Not a Power Pages project, skip
16+
if (!projectRoot) return approve(); // Not a Power Pages project, skip
1717

1818
// Server logic files live inside .powerpages-site/server-logic/
1919
const serverLogicDir = path.join(projectRoot, '.powerpages-site', 'server-logic');
20-
if (!fs.existsSync(serverLogicDir)) approve(); // No server-logic folder, not a server logic session
20+
if (!fs.existsSync(serverLogicDir)) return approve(); // No server-logic folder, not a server logic session
2121

2222
const logicDirs = findServerLogicDirs(serverLogicDir);
23-
if (logicDirs.length === 0) approve(); // No server logic subdirectories, skip
23+
if (logicDirs.length === 0) return approve(); // No server logic subdirectories, skip
2424

2525
const errors = [];
2626

@@ -42,28 +42,54 @@ runValidation((cwd) => {
4242
// Validate YAML contents
4343
const ymlContent = fs.readFileSync(ymlFile, 'utf8');
4444

45-
// Check id field exists and is a valid UUID
45+
// Check id field exists and is a valid UUID (strip surrounding quotes if present)
4646
const idMatch = ymlContent.match(/^id:\s*(.+)$/m);
4747
if (!idMatch) {
4848
errors.push(`${dirName}.serverlogic.yml: missing 'id' field — PAC CLI requires a GUID`);
49-
} else if (!UUID_REGEX.test(idMatch[1].trim())) {
50-
errors.push(`${dirName}.serverlogic.yml: 'id' is not a valid UUID: ${idMatch[1].trim()}`);
49+
} else {
50+
const idValue = idMatch[1].trim().replace(/^['"]|['"]$/g, '');
51+
if (!UUID_REGEX.test(idValue)) {
52+
errors.push(`${dirName}.serverlogic.yml: 'id' is not a valid UUID: ${idValue}`);
53+
}
5154
}
5255

53-
// Check adx_serverlogic_adx_webrole is present and non-empty
54-
if (!ymlContent.includes('adx_serverlogic_adx_webrole:')) {
56+
// Check adx_serverlogic_adx_webrole is present and non-empty, and validate GUIDs
57+
const webRoleHeaderMatch = /^adx_serverlogic_adx_webrole:\s*$/m.exec(ymlContent);
58+
if (!webRoleHeaderMatch) {
5559
errors.push(`${dirName}.serverlogic.yml: missing 'adx_serverlogic_adx_webrole' field — at least one web role is required`);
5660
} else {
57-
const roleMatches = ymlContent.match(/^\s+-\s+\S+/gm);
58-
if (!roleMatches || roleMatches.length === 0) {
61+
const sectionStart = webRoleHeaderMatch.index + webRoleHeaderMatch[0].length;
62+
const rest = ymlContent.slice(sectionStart);
63+
const nextKeyMatch = rest.match(/^[A-Za-z0-9_]+:\s*/m);
64+
const sectionEnd = nextKeyMatch ? sectionStart + nextKeyMatch.index : ymlContent.length;
65+
const webRoleSection = ymlContent.slice(sectionStart, sectionEnd);
66+
67+
const roleItemRegex = /^\s*-\s+([^\s#]+)/gm;
68+
let match;
69+
let hasItems = false;
70+
71+
while ((match = roleItemRegex.exec(webRoleSection)) !== null) {
72+
hasItems = true;
73+
const roleValue = match[1].trim().replace(/^['"]|['"]$/g, '');
74+
if (!UUID_REGEX.test(roleValue)) {
75+
errors.push(`${dirName}.serverlogic.yml: web role value '${roleValue}' under 'adx_serverlogic_adx_webrole' is not a valid UUID`);
76+
}
77+
}
78+
79+
if (!hasItems) {
5980
errors.push(`${dirName}.serverlogic.yml: 'adx_serverlogic_adx_webrole' array is empty — at least one web role GUID is required`);
6081
}
6182
}
6283

63-
// Check name field matches directory name
84+
// Check name field exists and matches directory name (strip surrounding quotes if present)
6485
const nameMatch = ymlContent.match(/^name:\s*(.+)$/m);
65-
if (nameMatch && nameMatch[1].trim() !== dirName) {
66-
errors.push(`${dirName}.serverlogic.yml: 'name' field '${nameMatch[1].trim()}' does not match folder name '${dirName}'`);
86+
if (!nameMatch) {
87+
errors.push(`${dirName}.serverlogic.yml: missing 'name' field — it must be present and match the folder name '${dirName}'`);
88+
} else {
89+
const nameValue = nameMatch[1].trim().replace(/^['"]|['"]$/g, '');
90+
if (nameValue !== dirName) {
91+
errors.push(`${dirName}.serverlogic.yml: 'name' field '${nameValue}' does not match folder name '${dirName}'`);
92+
}
6793
}
6894
}
6995

@@ -81,13 +107,37 @@ runValidation((cwd) => {
81107
continue;
82108
}
83109

84-
// Check: each function has try/catch
110+
// Check: no disallowed top-level functions outside the allowlist
111+
const topLevelFnRegex = /(?:^|\n)\s*(?:async\s+)?function\s+([a-zA-Z0-9_]+)\s*\(/g;
112+
let fnMatch;
113+
const disallowedFunctions = new Set();
114+
while ((fnMatch = topLevelFnRegex.exec(content)) !== null) {
115+
if (!ALLOWED_FUNCTIONS.includes(fnMatch[1])) {
116+
disallowedFunctions.add(fnMatch[1]);
117+
}
118+
}
119+
const exportRegex = /(?:^|\n)\s*(?:module\.exports|exports)\.([a-zA-Z0-9_]+)\s*=/g;
120+
let exportMatch;
121+
while ((exportMatch = exportRegex.exec(content)) !== null) {
122+
if (!ALLOWED_FUNCTIONS.includes(exportMatch[1])) {
123+
disallowedFunctions.add(exportMatch[1]);
124+
}
125+
}
126+
if (disallowedFunctions.size > 0) {
127+
errors.push(`${dirName}.js: only get, post, put, patch, and del functions are allowed; found additional top-level functions: ${Array.from(disallowedFunctions).join(', ')}`);
128+
continue;
129+
}
130+
131+
// Check: each function has try/catch (scan until next top-level function or end of file)
85132
for (const fn of foundFunctions) {
86133
const fnRegex = new RegExp(`(?:async\\s+)?function\\s+${fn}\\s*\\([^)]*\\)\\s*\\{`, 'g');
87134
const match = fnRegex.exec(content);
88135
if (match) {
89-
const afterFn = content.substring(match.index + match[0].length, match.index + match[0].length + 100);
90-
if (!afterFn.includes('try')) {
136+
const bodyStart = match.index + match[0].length;
137+
const nextFnMatch = content.slice(bodyStart).match(/\n(?:async\s+)?function\s+[a-zA-Z]/);
138+
const bodyEnd = nextFnMatch ? bodyStart + nextFnMatch.index : content.length;
139+
const fnBody = content.slice(bodyStart, bodyEnd);
140+
if (!/\btry\s*\{/.test(fnBody)) {
91141
errors.push(`${dirName}.js: function '${fn}' is missing try/catch error handling`);
92142
}
93143
}
@@ -116,9 +166,9 @@ runValidation((cwd) => {
116166
}
117167
}
118168

119-
// Check: no console.log
169+
// Check: no console usage
120170
if (/\bconsole\s*\./.test(content)) {
121-
errors.push(`${dirName}.js: contains console.log — use Server.Logger instead`);
171+
errors.push(`${dirName}.js: contains console.* — use Server.Logger instead`);
122172
}
123173

124174
// Check: no 'function delete()'
@@ -142,6 +192,8 @@ function findServerLogicDirs(dir) {
142192
dirs.push(path.join(dir, entry.name));
143193
}
144194
}
145-
} catch {}
195+
} catch (err) {
196+
throw new Error(`Failed to read server logic directory '${dir}': ${err.message}`);
197+
}
146198
return dirs;
147199
}

0 commit comments

Comments
 (0)