Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions plugins/power-pages/scripts/tests/validate-serverlogic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,42 @@ test('yml name mismatch is flagged', (t) => {
assert.equal(result.status, 2);
assert.match(result.stderr, /does not match folder name/);
});

test('startswith( inside string literal is flagged (with-pattern)', (t) => {
const projectRoot = createTempProject(t);
setupProject(projectRoot);
const startsWithJs = `function get() {
try {
Server.Logger.Log("test-endpoint GET called");
var query = "$filter=startswith(name,'INV-')";
return JSON.stringify({ status: "success", query: query });
} catch (err) {
Server.Logger.Error("test-endpoint GET failed: " + err.message);
return JSON.stringify({ status: "error", message: err.message });
}
}`;
writeServerLogic(projectRoot, 'test-endpoint', startsWithJs, VALID_YML);

const result = runValidator(projectRoot);
assert.equal(result.status, 2);
assert.match(result.stderr, /contains the substring 'with\('/);
});

test('split startswith( workaround passes validation', (t) => {
const projectRoot = createTempProject(t);
setupProject(projectRoot);
const splitJs = `function get() {
try {
Server.Logger.Log("test-endpoint GET called");
var query = "$filter=startswith" + "(name,'INV-')";
return JSON.stringify({ status: "success", query: query });
} catch (err) {
Server.Logger.Error("test-endpoint GET failed: " + err.message);
return JSON.stringify({ status: "error", message: err.message });
}
}`;
writeServerLogic(projectRoot, 'test-endpoint', splitJs, VALID_YML);

const result = runValidator(projectRoot);
assert.equal(result.status, 0, result.stderr);
});
61 changes: 61 additions & 0 deletions plugins/power-pages/skills/add-server-logic/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,26 @@ Repeat this step for each approved server logic item. Create or update `<PROJECT
6. **No browser APIs**: No `fetch`, `XMLHttpRequest`, `setTimeout`, `setInterval`, `console.log`, or DOM APIs.
7. **Async when needed**: Mark functions as `async` only when they use `await` (HttpClient calls). Dataverse connector methods (`Server.Connector.Dataverse.*`) are **synchronous** — do NOT use `async`/`await` with them.

#### Prohibited Script Patterns

The Power Pages server-side script validator rejects scripts containing certain patterns at runtime. Violations surface as `RTSL01: Script validation failed: prohibited pattern found - Pattern: <regex>` in diagnostics, and the function silently falls through without executing user code.

| Pattern | Regex | Caveat |
|---------|-------|--------|
| JavaScript `with` statement | `with\s*\(` | The regex matches the substring `with(` **anywhere** in the file — including inside string literals and inside other identifiers. OData filter functions like `startswith(`, `endswith(`, and `groupwith(` will trip it because they end with `with(`. |

**Workaround for OData functions** — split the literal so `with(` is not contiguous in source:
Comment thread
priyanshu92 marked this conversation as resolved.

```javascript
// ❌ Triggers validator: "startswith(" contains the substring "with("
var query = "$filter=startswith(name,'INV-')";

// ✅ Split the literal — server still receives "startswith(name,...)"
var query = "$filter=startswith" + "(name,'INV-')";
```

The same trick applies to `endswith(`, `groupwith(`, and any other identifier that ends with `with(`.

#### Code Template

```javascript
Expand Down Expand Up @@ -1112,6 +1132,7 @@ Provide testing instructions:
Use the frontend integration reference from Phase 9 for the exact calling pattern that matches the site's stack.

5. **Check diagnostics** — Server.Logger output can be viewed in Power Pages design studio diagnostics
6. **If the endpoint returns an error or unexpected response** — see [Troubleshooting Server Logic Execution Errors](#troubleshooting-server-logic-execution-errors) for the Playwright + `X-Ms-UserTrace` debugging flow

**Output**: Code validated, API URL provided, test guidance given

Expand Down Expand Up @@ -1188,6 +1209,46 @@ After deployment (or if skipped), remind the user:

---

## Troubleshooting Server Logic Execution Errors

When a deployed server logic endpoint returns an error or unexpected response, the underlying cause is usually hidden inside the `X-Ms-UserTrace` response header — a base64-encoded blob containing the runtime diagnostic logs. The Power Pages Edge browser extension shows the same data, but inspecting the response header is the fastest path when iterating against a live site.

Use this flow whenever a server logic call fails or returns a different response than expected:

### 1. Open the Live Site in a Browser via Playwright

Use the Playwright MCP tools to drive the site:

1. Navigate to the deployed site URL (the `websiteUrl` returned by `/activate-site` or shown in the Power Pages admin center).
2. Ask the user to sign in if the endpoint requires authentication and wait for confirmation.
3. Trigger the action that calls the failing server logic endpoint (click the button, submit the form, etc.) — or call the endpoint directly with `fetch()`.

### 2. Capture the Network Response

Use `mcp__plugin_power-pages_playwright__browser_network_requests` to list network activity, then locate the request to `/_api/serverlogics/<name>`. Note:

- The HTTP status code (e.g., 200, 400, 500)
- The response body (often a generic error or empty payload when validation fails)
- **Most importantly: the `X-Ms-UserTrace` response header** — this is where the actual diagnostic logs live

If `browser_network_requests` does not surface the response headers directly, fall back to `mcp__plugin_power-pages_playwright__browser_evaluate` and read the headers from a `fetch()` call:

```javascript
const res = await fetch('/_api/serverlogics/<name>', { method: 'GET', credentials: 'include' });
const trace = res.headers.get('X-Ms-UserTrace');
return { status: res.status, body: await res.text(), trace };
```

### 3. Decode the `X-Ms-UserTrace` Header

The header value is base64-encoded JSON. Decode it.

The decoded payload contains the diagnostic log entries — including the actual error message, the prohibited pattern (if script validation failed).

After fixing, redeploy via `/deploy-site` and restart the site so the change is picked up immediately.

---

## Important Notes

### Throughout All Phases
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,24 @@ runValidation((cwd) => {
if (/(?:async\s+)?function\s+delete\s*\(/m.test(strippedContent)) {
errors.push(`${dirName}.js: uses 'function delete()' — 'delete' is a reserved word, use 'del' instead`);
}

// Check: no 'with(' substring anywhere in the raw source.
// The Power Pages server validator rejects scripts matching /with\s*\(/ to block
// the JavaScript `with` statement, but the regex also matches the substring inside
// identifiers like `startswith(`, `endswith(`, and `groupwith(` — even inside string
// literals — which causes runtime error: "Script validation failed: prohibited
// pattern found - Pattern: with\s*\(". Run this on the raw content (not stripped)
// because the server validator does the same.
const withMatches = [...content.matchAll(/with\s*\(/g)];
if (withMatches.length > 0) {
const lineNumbers = withMatches.map(m => content.slice(0, m.index).split('\n').length);
errors.push(
`${dirName}.js: contains the substring 'with(' on line(s) ${lineNumbers.join(', ')} — ` +
`the Power Pages server validator's regex /with\\s*\\(/ blocks this even inside string literals ` +
`(e.g., OData functions like startswith(, endswith(). Split the literal so 'with(' is not contiguous, ` +
Comment thread
priyanshu92 marked this conversation as resolved.
`e.g., \`"startswith" + "(crd50_name,..."\` instead of \`"startswith(crd50_name,..."\`.`
Comment thread
priyanshu92 marked this conversation as resolved.
);
}
}

if (errors.length > 0) {
Expand Down
Loading