diff --git a/.github/workflows/ensure-skill-version-check.yml b/.github/workflows/ensure-skill-version-check.yml new file mode 100644 index 000000000..c78f6a189 --- /dev/null +++ b/.github/workflows/ensure-skill-version-check.yml @@ -0,0 +1,58 @@ +name: validate-skill-version-check + +on: + pull_request: + branches: + - main + paths: + - "plugins/power-pages/skills/**" + +permissions: + contents: write + +jobs: + validate-skill-version-check: + name: validate-skill-version-check + runs-on: ubuntu-latest + steps: + - name: generate app token + if: ${{ github.event.pull_request.head.repo.fork == false }} + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: 3189942 + private-key: ${{ secrets.POWER_PLATFORM_SKILLS_APP_PRIVATE_KEY }} + + - name: checkout (non-fork) + if: ${{ github.event.pull_request.head.repo.fork == false }} + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: checkout (fork) + if: ${{ github.event.pull_request.head.repo.fork == true }} + uses: actions/checkout@v4 + + - name: setup-node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: check-only (fork) + if: ${{ github.event.pull_request.head.repo.fork == true }} + run: node scripts/ensure-skill-version-check.js --check + + - name: add missing version checks (non-fork) + if: ${{ github.event.pull_request.head.repo.fork == false }} + run: node scripts/ensure-skill-version-check.js + + - name: commit and push if changed (non-fork) + if: ${{ github.event.pull_request.head.repo.fork == false }} + run: | + git diff --quiet && exit 0 + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add "plugins/power-pages/skills/*/SKILL.md" + git commit -m "Auto-add plugin version check to SKILL.md files" + git push diff --git a/plugins/power-pages/.claude-plugin/plugin.json b/plugins/power-pages/.claude-plugin/plugin.json index b1b60b616..7bed12d64 100644 --- a/plugins/power-pages/.claude-plugin/plugin.json +++ b/plugins/power-pages/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "power-pages", - "version": "1.1.2", + "version": "1.2.0", "description": "Create and deploy Power Pages sites using modern development approaches. Supports code sites (SPAs) with React, Angular, Vue, or Astro, with more site types coming soon.", "author": { "name": "Microsoft", diff --git a/plugins/power-pages/AGENTS.md b/plugins/power-pages/AGENTS.md index c3ce179f7..d3a4cc088 100644 --- a/plugins/power-pages/AGENTS.md +++ b/plugins/power-pages/AGENTS.md @@ -44,7 +44,17 @@ model: opus --- ``` -Note: `allowed-tools` must be a comma-separated list, not JSON array or YAML list syntax. +Note: `allowed-tools` must be a comma-separated list, not JSON array or YAML list syntax. Do not add `hooks` to skill frontmatter; Power Pages skills register lifecycle hooks centrally. + +### Plugin Version Check + +Every SKILL.md must include the following line immediately after the closing `---` of the frontmatter (before the `#` title): + +```markdown +> **Plugin check**: Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding. +``` + +This runs a lightweight check comparing the local plugin version against `origin/main` and shows an update notice if a newer version is available. ### Key Patterns @@ -58,6 +68,16 @@ Note: `allowed-tools` must be a comma-separated list, not JSON array or YAML lis - **Skill tracking** — Every skill must record usage in its final phase via `> Reference: ${CLAUDE_PLUGIN_ROOT}/references/skill-tracking-reference.md` (pointer pattern, not hardcoded command). When adding a new skill, also add its entry to the skill name mapping table in `references/skill-tracking-reference.md`. - **Dataverse API calls** — Use deterministic Node.js scripts (in the skill's `scripts/` directory) for Dataverse API queries. Scripts should import `getAuthToken` and `makeRequest` from `scripts/lib/validation-helpers.js`. Never use inline PowerShell `Invoke-RestMethod` for API calls — scripts are more reliable, testable, and cross-platform. +## Common Review Pitfalls + +These patterns have caused repeated PR review feedback. Check for them before submitting changes to skills, validators, or hooks. + +- **Phase cross-references break silently** — When renumbering or reordering phases in a SKILL.md, also update: `references/` docs that mention phase numbers, the Key Decision Points section, and any other files that cross-reference this skill's phases. After any phase reorder, grep for the old phase number across the skill directory and its references. +- **Validators must match the exact constraint** — If the rule is "no exports at all", block all `module.exports`/`exports` — don't just check if exported names are in an allowlist. If the rule is "try/catch required", verify both `try` AND `catch` exist. Re-read the exact constraint from SKILL.md and test the boundary cases. +- **Hook scripts run on every Skill tool use** — The PostToolUse hook fires for all tracked skills, so unconditional `process.stderr.write` creates noise. Gate debug logging behind `process.env.DEBUG`. Only errors should go to stderr unconditionally. +- **Template placeholders in ` + + + diff --git a/plugins/power-pages/skills/add-cloud-flow/scripts/create-cloud-flow-metadata.js b/plugins/power-pages/skills/add-cloud-flow/scripts/create-cloud-flow-metadata.js new file mode 100644 index 000000000..ff3784cb0 --- /dev/null +++ b/plugins/power-pages/skills/add-cloud-flow/scripts/create-cloud-flow-metadata.js @@ -0,0 +1,137 @@ +#!/usr/bin/env node + +// Creates a cloud flow consumer metadata YAML file for Power Pages code sites. +// +// Field naming follows PAPortalCommon.cs ConvertToReadableJson (git format): +// - adx_* scalar fields have the "adx_" prefix stripped +// - adx_cloudflowconsumerid → id (primary key) +// - adx_name → name (set to the flow's display name, not a slug) +// - adx_processid → processid +// - adx_flowapiurl → flowapiurl +// - adx_flowtriggerurl → flowtriggerurl +// - adx_metadata → metadata +// - adx_websiteid is NOT written — not required for code sites +// - M2M relationship adx_CloudFlowConsumer_adx_webrole kept as-is +// - statecode/statuscode defaults omitted +// - Empty string values written as '' +// - Fields sorted alphabetically +// +// Usage: +// node create-cloud-flow-metadata.js \ +// --fileSlug used for the filename only +// --flowName flow display name → written as the "name" field +// --flowId workflow entity ID → "processid" +// --flowTriggerUrl trigger callback URL (blank until deployed) +// --flowApiUrl /_api/cloudflow/v1.0/trigger/ +// --webRoleIds comma-separated web role UUIDs +// [--metadata ] optional metadata string +// +// Output (JSON to stdout): +// { "id": "", "filePath": "" } +// +// Exits with code 1 on validation errors (messages to stderr). + +const fs = require('fs'); +const path = require('path'); +const generateUuid = require(path.join(__dirname, '..', '..', '..', 'scripts', 'generate-uuid')); + +const args = process.argv.slice(2); + +function getArg(name) { + const idx = args.indexOf(`--${name}`); + return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null; +} + +const projectRoot = getArg('projectRoot'); +const fileSlug = getArg('fileSlug')?.trim() || null; +const flowName = getArg('flowName')?.trim() || null; +const flowId = getArg('flowId')?.trim() || null; +const flowTriggerUrl = getArg('flowTriggerUrl')?.trim() ?? ''; +const flowApiUrl = getArg('flowApiUrl')?.trim() ?? ''; +const webRoleIdsRaw = getArg('webRoleIds'); +const metadata = getArg('metadata')?.trim() ?? ''; + +const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +if (!projectRoot || !fileSlug || !flowName || !flowId || !webRoleIdsRaw) { + process.stderr.write( + 'Usage: node create-cloud-flow-metadata.js --projectRoot --fileSlug ' + + '--flowName --flowId --flowTriggerUrl --flowApiUrl ' + + '--webRoleIds [--metadata ]\n' + ); + process.exit(1); +} + +// Validate fileSlug: lowercase, hyphenated, max 50 chars (per SKILL.md contract) +if (!/^[a-z0-9][a-z0-9-]*$/.test(fileSlug)) { + process.stderr.write( + `Error: --fileSlug must be lowercase alphanumeric with hyphens only (no leading hyphen). Got: "${fileSlug}"\n` + ); + process.exit(1); +} +if (fileSlug.length > 50) { + process.stderr.write( + `Error: --fileSlug must be at most 50 characters. Got ${fileSlug.length} characters.\n` + ); + process.exit(1); +} + +if (!UUID_REGEX.test(flowId)) { + process.stderr.write(`Error: --flowId must be a valid UUID. Got: "${flowId}"\n`); + process.exit(1); +} + +const webRoleIds = webRoleIdsRaw.split(',').map(id => id.trim()).filter(Boolean); +if (webRoleIds.length === 0) { + process.stderr.write('Error: --webRoleIds must contain at least one UUID\n'); + process.exit(1); +} + +for (const roleId of webRoleIds) { + if (!UUID_REGEX.test(roleId)) { + process.stderr.write(`Error: Invalid UUID in --webRoleIds: "${roleId}"\n`); + process.exit(1); + } +} + +const cloudFlowDir = path.join(projectRoot, '.powerpages-site', 'cloud-flow-consumer'); +if (!fs.existsSync(cloudFlowDir)) { + fs.mkdirSync(cloudFlowDir, { recursive: true }); +} + +const fileName = `${fileSlug}.cloudflowconsumer.yml`; +const filePath = path.join(cloudFlowDir, fileName); + +if (fs.existsSync(filePath)) { + process.stderr.write(`Error: Cloud flow consumer metadata file already exists at ${filePath}\n`); + process.exit(1); +} + +const uuid = generateUuid(); + +// Serialize a string value safely for YAML: always single-quote, escaping internal single quotes. +// Rejects newlines since single-quoted YAML scalars cannot span lines without breaking structure. +function yamlStr(val) { + if (/[\r\n]/.test(val)) { + process.stderr.write(`Error: Value contains newline characters which are not supported in single-line YAML fields: "${val.slice(0, 50)}..."\n`); + process.exit(1); + } + return "'" + val.replace(/'/g, "''") + "'"; +} + +// Fields sorted alphabetically, matching PAPortalCommon.cs ConvertToReadableJson output. +// websiteid is intentionally omitted — not required for code sites. +const yamlLines = [ + 'adx_CloudFlowConsumer_adx_webrole:', + ...webRoleIds.map(id => ` - ${id}`), + `flowapiurl: ${yamlStr(flowApiUrl)}`, + `flowtriggerurl: ${yamlStr(flowTriggerUrl)}`, + `id: ${uuid}`, + `metadata: ${yamlStr(metadata)}`, + `name: ${yamlStr(flowName)}`, + `processid: ${flowId}`, + '', +]; + +fs.writeFileSync(filePath, yamlLines.join('\n'), 'utf8'); +process.stdout.write(JSON.stringify({ id: uuid, filePath })); diff --git a/plugins/power-pages/skills/add-cloud-flow/scripts/list-cloud-flows.js b/plugins/power-pages/skills/add-cloud-flow/scripts/list-cloud-flows.js new file mode 100644 index 000000000..d10a8677f --- /dev/null +++ b/plugins/power-pages/skills/add-cloud-flow/scripts/list-cloud-flows.js @@ -0,0 +1,144 @@ +#!/usr/bin/env node + +// Lists Power Automate cloud flows that have a PowerPages trigger, matching +// how Power Pages Studio discovers flows via the Flow RP API. +// +// The studio calls: +// GET https://{flow-rp}/providers/Microsoft.ProcessSimple/environments/{environmentId}/flows +// ?api-version=2016-11-01-beta +// &$filter=properties/definitionSummary/triggers/any(t: t/kind eq 'powerpages') +// &$top=50 +// &include=includeSolutionCloudFlows +// with pagination via nextLink. +// +// Usage: +// node list-cloud-flows.js +// +// Output (JSON to stdout): +// { "flows": [ { "id": "", "flowRpName": "", "displayName": "", "description": "", "state": "Active|Draft" } ] } +// +// Exits with code 1 on errors (messages to stderr). + +const path = require('path'); +const { getAuthToken, makeRequest, getPacAuthInfo } = require( + path.join(__dirname, '..', '..', '..', 'scripts', 'lib', 'validation-helpers') +); + +// Flow RP base URL by cloud region (mirrors studio's KnownServiceNames.Flow resolution) +const CLOUD_TO_FLOW_RP = { + 'Public': 'https://api.flow.microsoft.com', + 'UsGov': 'https://gov.api.flow.microsoft.us', + 'UsGovHigh': 'https://high.gov.api.flow.microsoft.us', + 'UsGovDod': 'https://dod.api.flow.microsoft.us', + 'China': 'https://api.flow.microsoft.cn', +}; + +// OAuth resource URL for the Flow service by cloud +const CLOUD_TO_FLOW_RESOURCE = { + 'Public': 'https://service.flow.microsoft.com/', + 'UsGov': 'https://gov.service.flow.microsoft.us/', + 'UsGovHigh': 'https://high.gov.service.flow.microsoft.us/', + 'UsGovDod': 'https://dod.service.flow.microsoft.us/', + 'China': 'https://service.flow.microsoft.cn/', +}; + +(async () => { + const authInfo = getPacAuthInfo(); + if (!authInfo) { + process.stderr.write( + 'Error: Unable to determine environment. Run `pac auth who` to verify PAC CLI is authenticated.\n' + ); + process.exit(1); + } + + const { environmentId, cloud } = authInfo; + + const flowRpBase = CLOUD_TO_FLOW_RP[cloud] || CLOUD_TO_FLOW_RP['Public']; + const flowResource = CLOUD_TO_FLOW_RESOURCE[cloud] || CLOUD_TO_FLOW_RESOURCE['Public']; + + const token = getAuthToken(flowResource); + if (!token) { + process.stderr.write( + 'Error: Unable to obtain access token for the Power Automate service.\n' + + 'Run `az login` and ensure your account has access to the environment.\n' + ); + process.exit(1); + } + + const API_VERSION = '2016-11-01-beta'; + const FILTER = "properties/definitionSummary/triggers/any(t: t/kind eq 'powerpages')"; + + const baseUrl = + `${flowRpBase}/providers/Microsoft.ProcessSimple/environments/${environmentId}/flows` + + `?api-version=${encodeURIComponent(API_VERSION)}` + + `&$filter=${encodeURIComponent(FILTER)}` + + `&$top=50` + + `&include=includeSolutionCloudFlows`; + + const flows = []; + let nextUrl = baseUrl; + + while (nextUrl) { + const response = await makeRequest({ + url: nextUrl, + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + timeout: 20000, + }); + + if (response.error) { + process.stderr.write(`Error calling Flow RP API: ${response.error}\n`); + process.exit(1); + } + + if (response.statusCode === 401) { + process.stderr.write( + 'Error: Unauthorized. Ensure your Azure CLI token is valid and has access to Power Automate.\n' + ); + process.exit(1); + } + + if (response.statusCode < 200 || response.statusCode >= 300) { + process.stderr.write( + `Error: Flow RP API returned HTTP ${response.statusCode}.\n${response.body}\n` + ); + process.exit(1); + } + + let parsed; + try { + parsed = JSON.parse(response.body); + } catch { + process.stderr.write(`Error: Invalid JSON response from Flow RP API.\n${response.body}\n`); + process.exit(1); + } + + for (const flow of parsed.value || []) { + const props = flow.properties || {}; + const statecode = props.state === 'Started' ? 'Active' : 'Draft'; + flows.push({ + id: props.workflowEntityId, // Dataverse workflow entity ID — used as processid in YAML + flowRpName: flow.name, // Flow RP identifier — used when calling installFlow API + displayName: props.displayName || flow.name, + description: props.description || '', + state: statecode, + }); + } + + // Follow pagination link if present + nextUrl = parsed.nextLink || null; + } + + if (flows.length === 0) { + process.stderr.write( + 'No Power Automate flows with a PowerPages trigger were found in this environment.\n' + + 'Create a flow in Power Automate with a "When a Power Pages flow step is run" trigger, then run this skill again.\n' + ); + process.exit(1); + } + + process.stdout.write(JSON.stringify({ flows }, null, 2)); +})(); diff --git a/plugins/power-pages/skills/add-cloud-flow/scripts/validate-cloudflow.js b/plugins/power-pages/skills/add-cloud-flow/scripts/validate-cloudflow.js new file mode 100644 index 000000000..6ef679383 --- /dev/null +++ b/plugins/power-pages/skills/add-cloud-flow/scripts/validate-cloudflow.js @@ -0,0 +1,123 @@ +#!/usr/bin/env node + +// Validates cloud flow consumer metadata files created by the add-cloud-flow skill. +// Checks all .cloudflowconsumer.yml files in .powerpages-site/cloud-flow-consumer/. +// Runs as a PostToolUse hook when the add-cloud-flow skill completes. + +const fs = require('fs'); +const path = require('path'); +const { + approve, + block, + runValidation, + findProjectRoot, + UUID_REGEX, +} = require('../../../scripts/lib/validation-helpers'); + +runValidation((cwd) => { + const projectRoot = findProjectRoot(cwd); + if (!projectRoot) return approve(); + + const cloudFlowDir = path.join(projectRoot, '.powerpages-site', 'cloud-flow-consumer'); + if (!fs.existsSync(cloudFlowDir)) return approve(); + + const flowFiles = fs + .readdirSync(cloudFlowDir, { withFileTypes: true }) + .filter((e) => e.isFile() && e.name.endsWith('.cloudflowconsumer.yml')) + .map((e) => path.join(cloudFlowDir, e.name)); + + if (flowFiles.length === 0) return approve(); + + const errors = []; + + for (const filePath of flowFiles) { + const fileName = path.basename(filePath); + const content = fs.readFileSync(filePath, 'utf8'); + + // id — must be a valid UUID + const idMatch = content.match(/^id:\s*(.+)$/m); + if (!idMatch) { + errors.push(`${fileName}: missing 'id' field`); + } else { + const val = idMatch[1].trim().replace(/^['"]|['"]$/g, ''); + if (!UUID_REGEX.test(val)) { + errors.push(`${fileName}: 'id' is not a valid UUID: ${val}`); + } + } + + // processid — must be a valid UUID + const processIdMatch = content.match(/^processid:\s*(.+)$/m); + if (!processIdMatch) { + errors.push(`${fileName}: missing 'processid' field`); + } else { + const val = processIdMatch[1].trim().replace(/^['"]|['"]$/g, ''); + if (!UUID_REGEX.test(val)) { + errors.push(`${fileName}: 'processid' is not a valid UUID: ${val}`); + } + } + + // name — must be present and non-empty + const nameMatch = content.match(/^name:\s*(.+)$/m); + if (!nameMatch || !nameMatch[1].trim()) { + errors.push(`${fileName}: missing or empty 'name' field`); + } + + // flowapiurl — must be present with a non-empty URL value + const flowApiMatch = content.match(/^flowapiurl:\s*(.*)$/m); + if (!flowApiMatch) { + errors.push(`${fileName}: missing 'flowapiurl' field`); + } else { + const val = flowApiMatch[1].trim().replace(/^['"]|['"]$/g, ''); + if (!val) { + errors.push(`${fileName}: 'flowapiurl' is empty — it must contain the cloud flow API endpoint URL`); + } + } + + // flowtriggerurl — must be present (value is always empty in Power Pages cloud flow consumer YAML) + if (!/^flowtriggerurl:/m.test(content)) { + errors.push(`${fileName}: missing 'flowtriggerurl' field`); + } + + // adx_CloudFlowConsumer_adx_webrole — must have at least one valid UUID + const webRoleHeader = /^adx_CloudFlowConsumer_adx_webrole:\s*$/m.exec(content); + if (!webRoleHeader) { + errors.push(`${fileName}: missing 'adx_CloudFlowConsumer_adx_webrole' — at least one web role is required`); + } else { + const sectionStart = webRoleHeader.index + webRoleHeader[0].length; + const rest = content.slice(sectionStart); + const nextKey = rest.match(/^[A-Za-z0-9_]+:/m); + const section = nextKey ? rest.slice(0, nextKey.index) : rest; + + const roleRegex = /^\s*-\s+([^\s#]+)/gm; + let match; + let hasItems = false; + while ((match = roleRegex.exec(section)) !== null) { + hasItems = true; + const val = match[1].trim().replace(/^['"]|['"]$/g, ''); + if (!UUID_REGEX.test(val)) { + errors.push(`${fileName}: web role '${val}' is not a valid UUID`); + } + } + if (!hasItems) { + errors.push(`${fileName}: 'adx_CloudFlowConsumer_adx_webrole' array is empty`); + } + } + + // No adx_-prefixed scalar fields (M2M key is allowed) + const adxScalars = content.match(/^adx_(?!CloudFlowConsumer_adx_webrole)[a-z_]+:/gim); + if (adxScalars) { + errors.push(`${fileName}: unexpected adx_-prefixed fields: ${[...new Set(adxScalars)].join(', ')}`); + } + + // statecode / statuscode must not be present + if (/^statecode:/m.test(content) || /^statuscode:/m.test(content)) { + errors.push(`${fileName}: 'statecode' and 'statuscode' must not be present in code-site YAML`); + } + } + + if (errors.length > 0) { + block('Cloud flow consumer validation failed:\n- ' + errors.join('\n- ')); + } + + approve(); +}); diff --git a/plugins/power-pages/skills/add-sample-data/SKILL.md b/plugins/power-pages/skills/add-sample-data/SKILL.md index 9fbd1f8ca..84f9581cf 100644 --- a/plugins/power-pages/skills/add-sample-data/SKILL.md +++ b/plugins/power-pages/skills/add-sample-data/SKILL.md @@ -11,6 +11,8 @@ allowed-tools: Read, Write, Bash, Grep, Glob, AskUserQuestion, Task, TaskCreate, model: sonnet --- +> **Plugin check**: Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding. + # Add Sample Data Populate Dataverse tables with sample records via OData API so users can test and demo their Power Pages sites. diff --git a/plugins/power-pages/skills/add-seo/SKILL.md b/plugins/power-pages/skills/add-seo/SKILL.md index ec6c1b480..c65dd9e23 100644 --- a/plugins/power-pages/skills/add-seo/SKILL.md +++ b/plugins/power-pages/skills/add-seo/SKILL.md @@ -11,6 +11,8 @@ allowed-tools: Read, Write, Edit, Grep, Glob, Bash, AskUserQuestion, Task, TaskC model: sonnet --- +> **Plugin check**: Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding. + # Add SEO Add essential SEO assets to a Power Pages code site: `robots.txt`, `sitemap.xml`, and meta tags. diff --git a/plugins/power-pages/skills/add-server-logic/SKILL.md b/plugins/power-pages/skills/add-server-logic/SKILL.md new file mode 100644 index 000000000..40c7adeb4 --- /dev/null +++ b/plugins/power-pages/skills/add-server-logic/SKILL.md @@ -0,0 +1,1235 @@ +--- +name: add-server-logic +description: > + This skill should be used when the user asks to "create server logic", "add server-side code", + "write server logic", "add server endpoint", "create API endpoint", "add backend logic", + "write server-side JavaScript", "integrate server logic", "add server logic function", + "add server-side processing", "create serverlogic", "add serverlogics", or wants to create, + edit, or manage Power Pages Server Logic files — server-side JavaScript that runs securely + on the Power Pages runtime. This skill orchestrates the full lifecycle: understanding + requirements, fetching latest documentation, implementing the server logic code, configuring + site settings, and deploying. Use this skill whenever the user mentions "server logic", + "server-side code", or wants to move logic from the browser to the server in their Power Pages site. +user-invocable: true +allowed-tools: Read, Write, Edit, Bash, Grep, Glob, AskUserQuestion, Skill, Task, TaskCreate, TaskUpdate, TaskList, mcp__plugin_power-pages_microsoft-learn__microsoft_docs_search, mcp__plugin_power-pages_microsoft-learn__microsoft_code_sample_search, mcp__plugin_power-pages_microsoft-learn__microsoft_docs_fetch +model: opus +--- + +> **Plugin check**: Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding. + +# Add Server Logic + +Create and manage one or more Power Pages Server Logic files — server-side JavaScript that runs securely on the Power Pages runtime, hidden from the browser and protected by web roles and table permissions. Server Logic enables secure external API integrations, Dataverse operations, and custom business logic without exposing sensitive code or credentials to the client. + +## Core Principles + +- **Microsoft Learn is the source of truth**: Always fetch the latest documentation before writing code. The Server Logic feature is in preview and the SDK may change — never rely on cached knowledge alone. +- **No browser APIs, no dependencies**: Server Logic runs in a sandboxed server environment with ECMAScript 2023 support. There is no `fetch`, `XMLHttpRequest`, `setTimeout`, or any DOM API. No npm packages are available. +- **Five functions only**: A server logic file can only export these top-level functions: `get`, `post`, `put`, `patch`, `del`. The name `delete` is a reserved word in JavaScript and cannot be used. +- **Always return a string**: Every function must return a string. Use `JSON.stringify()` when returning objects or arrays. +- **Use TaskCreate/TaskUpdate**: Track all progress throughout all phases — create the todo list upfront with all phases before starting any work. + +> **Prerequisites:** +> - An existing Power Pages code site created +> - The site **must** be deployed at least once (`.powerpages-site` folder must exist) — server logic files live inside `.powerpages-site/server-logic/`, so deployment is required before any server logic can be created + +**Initial request:** $ARGUMENTS + +--- + +## Workflow + +1. **Verify Site Exists** — Locate the Power Pages project, explore existing patterns, and verify prerequisites +2. **Understand Requirements** — Determine the user intent and whether the solution needs one or more server logic files +3. **Fetch Latest Documentation** — Query Microsoft Learn for the most current Server Logic SDK reference +4. **Review Implementation Plan** — Present the plan to the user and confirm before writing code +5. **Implement Server Logic** — Create the approved `.js` and `.serverlogic.yml` files in `.powerpages-site/server-logic//` +6. **Configure Table Permissions** — *(Conditional: only if Server.Connector.Dataverse is used)* Set up table permissions for Dataverse tables accessed by the server logic +7. **Manage Secrets & Environment Variables** — *(Conditional: only if the server logic requires secrets)* Store sensitive values securely using Azure Key Vault (recommended) or direct environment variables in Dataverse +8. **Configure Site Settings** — Set up ServerLogic site settings if needed +9. **Client-Side Integration** — Help wire the server logic into the site's frontend code +10. **Verify & Test Guidance** — Validate the code and provide testing instructions +11. **Review & Deploy** — Present summary and offer deployment + +--- + +## Phase 1: Verify Site Exists + +**Goal**: Locate the Power Pages project root and confirm prerequisites + +**Actions**: + +1. Create todo list with all 11 phases (see [Progress Tracking](#progress-tracking) table) + +### 1.1 Locate Project + +Look for `powerpages.config.json` in the current directory or immediate subdirectories + +**If not found**: Tell the user to create a site first with `/create-site`. + +### 1.2 Read Existing Config + +Read `powerpages.config.json` to get the site name and configuration: + +### 1.3 Detect Framework + +Read `package.json` to determine the frontend framework (React, Vue, Angular, or Astro). This is needed for Phase 8 (client-side integration guidance). See `${CLAUDE_PLUGIN_ROOT}/references/framework-conventions.md` for the full framework detection mapping. + +### 1.4 Explore Existing Server Logic and Frontend Code + +Use the **Explore agent** (via `Task` tool with `agent_type: "explore"`) to analyze the site for existing server logic patterns and frontend code that may call or need to call server logic endpoints. + +**Prompt for the Explore agent:** + +> "Analyze this Power Pages code site for server logic context. Check: +> 1. Does `.powerpages-site/server-logic/` exist? If yes, list all subdirectories and their .js files. Summarize what each server logic does (which functions it implements, what SDK features it uses). Also read the corresponding .serverlogic.yml files to check web role assignments. +> 2. Search the frontend source code (`src/**/*.{ts,tsx,js,jsx,vue,astro}`) for any existing calls to `/_api/serverlogics/` — these indicate server logic endpoints already being consumed. +> 3. Look for CSRF token handling patterns (`__RequestVerificationToken`, `_layout/tokenhtml`) — these show how the site currently makes authenticated API calls. +> 4. Check for any TODO/FIXME comments mentioning server logic, backend, or server-side processing. +> 5. Look for hardcoded API URLs, mock data, or placeholder fetch calls that might need to be replaced with server logic calls. +> 6. Check for any existing service layer or API utility files in `src/shared/`, `src/services/`, or similar directories that could be reused for server logic integration. +> 7. Read `.powerpages-site/web-roles/*.webrole.yml` files to list available web roles and their GUIDs — these are needed when creating the server logic metadata YAML. +> 8. For each existing server logic, assess whether it can be reused or safely extended for the requested capability instead of creating a brand-new server logic file. Call out any strong reuse candidates and explain why. +> Report all findings so we can avoid duplicating work and match existing patterns." + +From the Explore agent's findings, note: +- **Existing server logic files** — what's already implemented, and which ones are candidates for reuse or extension +- **Frontend calling patterns** — how the site makes API calls (match this pattern in Phase 9) +- **Existing service/utility files** — reuse these when adding client-side integration +- **Gaps** — frontend code that references server logic endpoints that don't exist yet + +### 1.5 Check Deployment Status (Mandatory) + +Look for the `.powerpages-site` folder: + + +**If not found**: The site **must** be deployed before server logic can be created — server logic files live inside `.powerpages-site/server-logic/`. Tell the user: + +> "The `.powerpages-site` folder was not found. Server logic files are stored inside this folder, so the site must be deployed at least once before creating server logic. Would you like to deploy now?" + +Use `AskUserQuestion`: + +| Question | Options | +|----------|---------| +| The `.powerpages-site` folder is required for server logic. Would you like to deploy the site now? | Yes, deploy now (Required), Cancel | + +**If "Yes, deploy now"**: Invoke `/deploy-site` first, then continue to Phase 2. + +**If "Cancel"**: Stop the workflow — server logic cannot be created without `.powerpages-site`. + +**Output**: Confirmed project root, `.powerpages-site` exists, existing server logic (if any), available web roles + +--- + +## Phase 2: Understand Requirements + +**Goal**: Determine the user intent, identify whether one or more server logic files are needed, and capture the required HTTP methods for each item + +**Actions**: + +### 2.1 Analyze User Request + +From the user's request, determine: + +- **Intent shape**: Does the request map to a single server logic or multiple server logic? +- **Reuse opportunities**: Can an existing server logic satisfy or be safely extended for part of the request? +- **Server logic inventory**: For each required server logic, capture the purpose, suggested endpoint name, and whether it should be reused, extended, or created new +- **HTTP methods needed**: Which of the 5 functions should be implemented for each server logic (`get`, `post`, `put`, `patch`, `del`) + +Prefer reuse or safe extension of an existing server logic when it already matches the domain, security model, and lifecycle of the requested capability. Only create a new server logic when reuse would make the existing file confusing, over-broad, or unsafe. + +Prefer multiple server logic files when the use case naturally separates into different responsibilities, security boundaries, or lifecycle concerns. Examples: + +- Separate read vs. write workflows with different web role requirements +- Distinct integrations with different external systems or site settings +- Independent business capabilities that would be harder to test or reason about if merged into one endpoint + +### 2.1.1 Identify Validate-and-Execute Patterns + +For each planned server logic item, determine whether it should **validate-and-execute** — meaning the server logic both validates a business rule AND performs the resulting Dataverse write, rather than just returning a validation result for the client to act on. + +A server logic item should validate-and-execute when **any** of these are true: + +| Condition | Example | +|-----------|---------| +| It enforces a state machine or lifecycle | Order status: Draft → Submitted → Approved | +| The write is conditional on a business rule that must be tamper-proof | "Only allow bid submission before the deadline" | +| The operation spans multiple tables atomically | Award a bid + reject all others + update event status | +| The write involves a computed or derived value | Server calculates a score and writes it | +| The client should not have direct write access to the field | Status fields with strict transition rules | + +For each validate-and-execute item, note: +- **Which Dataverse writes the server logic will perform** (UpdateRecord, CreateRecord, etc.) +- **Which fields are being written** — these fields should NOT be writable via Web API from the client +- **What the server logic returns to the client** — typically a success/failure result with the before/after state, NOT a validation flag that the client acts on + +**Anti-pattern to avoid**: A server logic item that only validates and returns `{ valid: true/false }`, expecting the client to make a separate Web API call to perform the write. This allows the client to skip validation and write directly. + +### 2.1.2 Discover Dataverse Custom Actions + +If any planned server logic item involves Dataverse operations, check whether the user's Dataverse environment has existing custom actions (Custom APIs or Custom Process Actions) that could be leveraged instead of building logic from scratch. + +**Step 1 — Fetch custom actions:** + +```powershell +node "${CLAUDE_PLUGIN_ROOT}/scripts/list-custom-actions.js" "" +``` + +The script outputs a JSON object with: +- `customApis` — Modern Custom APIs with full request parameters and response properties +- `customProcessActions` — Legacy Custom Process Actions (activated only) +- `total` — Total count of both types combined + +Each entry includes: `name`, `displayName`, `description`, `type` (`action` or `function`), `binding` (`unbound`, `entity`, or `entityCollection`), `boundEntity`, and `source` (`customApi` or `customProcessAction`). Custom APIs also include `requestParameters` and `responseProperties` arrays. + +**Step 2 — Present and ask the user:** + +If custom actions are found (`total > 0`), present a summary to the user grouped by binding type (unbound vs. entity-bound) and ask whether any should be used: + +Use `AskUserQuestion`: + +| Question | Options | +|----------|---------| +| Your Dataverse environment has `` custom action(s). Would you like to use any of these in your server logic instead of writing the logic from scratch? | Yes, let me choose which ones to use; No, build everything from scratch | + +Present the list clearly — for each action show: name, description, type (action/function), binding, and bound entity (if applicable). Group them as **Unbound** and **Entity-bound** for readability. + +If the user says **No**, skip to Phase 2.2. + +**Step 3 — Map custom actions to server logic items:** + +If the user says **Yes**, for each server logic item being created, ask which custom action (if any) it should wrap: + +Use `AskUserQuestion` for each server logic item: + +| Question | Context | +|----------|---------| +| For the `` endpoint, which custom action should it use? | Present the list of custom actions with their names, descriptions, and binding types. Include **"None — build from scratch"** as an option. | + +Record the mapping for each server logic item. For items that wrap a custom action, note: +- The custom action name (used in the `InvokeCustomApi` call) +- Whether it's a function (`GET`) or action (`POST`) +- The binding type and bound entity (if applicable) +- The request parameters and response properties (if available from Custom APIs) + +This mapping will be used in Phase 5.3 when generating the server logic code, and will appear in the HTML plan (Phase 4) to indicate which items wrap existing custom actions. + +### 2.2 Identify SDK Features Needed + +Based on each planned server logic item's purpose, identify which Server SDK features are required: + +| Feature | When to use | +|---------|-------------| +| `Server.Connector.HttpClient` | Calling external REST APIs (NOT Dataverse) | +| `Server.Connector.Dataverse` | Reading/writing Dataverse records (CRUD + `InvokeCustomApi` for Dataverse Custom APIs) | +| `Server.Context` | Accessing request parameters, headers, body | +| `Server.User` | User-scoped operations, role checks | +| `Server.Logger` | Always — every function should log entry/exit and errors | +| `Server.Sitesetting` | Reading site setting configuration values | +| `Server.EnvironmentVariable` | Reading Dataverse environment variable values directly via `Server.EnvironmentVariable.get(name)` — an alternative to `Server.Sitesetting` for non-secret config | +| `Server.Website` | Accessing site metadata | + +### 2.3 Identify Secret Values + +Determine whether any server logic item requires secret or sensitive configuration values that should not be hardcoded. Common examples: + +| Scenario | Secret needed | +|----------|---------------| +| Calling an authenticated external API | API key, client secret, bearer token | +| Connecting to a third-party service | Connection string, access token | +| OAuth2 client credentials flow | Client ID + client secret | +| Webhook verification | Signing secret, shared key | + +For each identified secret, capture: +- **Secret name**: A descriptive name (e.g., `ExchangeRateApiKey`, `PaymentGatewaySecret`) +- **Purpose**: Why the secret is needed +- **Site setting name**: The name the server logic will use with `Server.Sitesetting.Get()` (e.g., `ExternalApi/ExchangeRateApiKey`) +- **Environment variable schema name**: The Dataverse environment variable schema name (e.g., `cr5b4_ExchangeRateApiKey`) + +These values will be used in Phase 7 to create the environment variables and site settings. + +### 2.3.1 Key Vault Decision + +If secrets were identified in Phase 2.3, ask the user now whether they want to use Azure Key Vault. This decision must happen before Phase 4 so the implementation plan can show the chosen secret management approach. + +Use `AskUserQuestion`: + +| Question | Options | +|----------|---------| +| This server logic requires secret values (e.g., API keys, client secrets). Azure Key Vault is the recommended way to store secrets securely. Would you like to use Azure Key Vault? | Yes, use Azure Key Vault (Recommended), No, store directly as environment variable | + +Record the user's choice — it will be shown in the HTML plan (Phase 4) and executed in Phase 7. + +### 2.4 Confirm with User + +If the requirements are ambiguous, use `AskUserQuestion` to clarify: + +| Question | Context | +|----------|---------| +| What should this server logic solution do overall? | If the purpose is unclear | +| Should this be one server logic or multiple server logic? | If the request could reasonably be modeled either way | +| Which HTTP methods does each server logic need? | If not specified — suggest based on the use case (e.g., read-only = GET, form processing = POST) | +| Does each server logic need to call external APIs, Dataverse, or both? | Determines which connectors to use | +| What should each server logic be named? | Suggest URL-friendly names based on the responsibilities | +| Does the server logic need any secret or sensitive values (API keys, client secrets, tokens)? | If the server logic calls authenticated external APIs or services | + +**Output**: Clear understanding of the overall intent, the list of server logic items to reuse/extend/create, their HTTP methods, SDK features needed, and any secrets required + +--- + +## Phase 3: Fetch Latest Documentation + +**Goal**: Discover and read all current Server Logic documentation from Microsoft Learn before writing any code + +This step is critical because Server Logic is a preview feature and the SDK surface may change. The documentation on Microsoft Learn is the authoritative source. + +**Actions**: + +### 3.1 Follow the Documentation Reference + +Use the reference document below as the source of truth for how to discover, classify, fetch, and reconcile Server Logic documentation: + +> Reference: `${CLAUDE_PLUGIN_ROOT}/skills/add-server-logic/references/server-logic-docs.md` + +Follow that reference to: + +- Search Microsoft Learn dynamically for all current Server Logic docs +- Fetch the core reference pages and any relevant scenario-specific pages +- Search for current code samples +- Reconcile the discovered documentation with the known SDK baseline in the reference + +### 3.2 Extract Task-Specific Notes + +From the fetched docs, extract and note the items that matter for the current task: + +- All SDK method signatures, parameter types, and return types +- Current supported HTTP methods and function signatures +- Site settings and their defaults +- Security model (web roles, table permissions, CSRF) +- Client-side calling patterns and response format +- Any new methods or breaking changes discovered in Microsoft Learn + +**Output**: Up-to-date SDK reference verified against all relevant Microsoft Learn documentation pages + +--- + +## Phase 4: Review Implementation Plan + +**Goal**: Present the implementation plan to the user and confirm before writing any code + +**Actions**: + +### 4.1 Prepare the Plan Data + +Build the server logic plan data and render the HTML plan before asking for approval. + +> Reference: `${CLAUDE_PLUGIN_ROOT}/skills/add-server-logic/references/server-logic-plan-data-format.md` + +The rendered plan should summarize: + +- The number of server logic items being created or reused +- Each endpoint name, API URL, and files to be created +- The functions that will be implemented and what each one does +- The SDK features, external services, and Dataverse tables involved for each item +- The web roles, security constraints, and site settings that apply to each item +- Any secrets or sensitive values that will be stored as environment variables (with or without Azure Key Vault). If the user chose Azure Key Vault in Phase 2.3.1, populate `SECRETS_DATA` with `useKeyVault: true` and the list of secrets — the HTML plan will render a Key Vault banner explaining the security benefits and show which secrets each server logic depends on. If no secrets are needed, set `SECRETS_DATA` to `null`. +- The expected next steps after approval + +### 4.2 Render the HTML Plan + +Generate the HTML plan file from the template and open it in the user's default browser before asking for approval. + +When working inside a Power Pages project, write the plan to: + +```text +/docs/serverlogic-plan.html +``` + +Create the `docs/` folder if it does not already exist. Keep this HTML file inside the repository so it can be reviewed and committed with the rest of the server logic work. + +Do **not** hand-author the HTML. Use the render script: + +```powershell +node "${CLAUDE_PLUGIN_ROOT}/scripts/render-serverlogic-plan.js" --output "" --data "" +``` + +The render script refuses to overwrite existing files. Before calling it, check if the default output path (`/docs/serverlogic-plan.html`) already exists. If it does, choose a new descriptive filename based on context — e.g., `serverlogic-plan-exchange-rate.html`, `serverlogic-plan-apr-2026.html`. Pass the chosen name via `--output`. + +### 4.3 Present Plan Summary + +Do **not** present a second detailed plan in the CLI. The HTML file is the single detailed plan artifact. + +In the CLI, give only a brief summary that points the user to the HTML plan open in the browser. Keep it to: + +- Total server logic count +- Whether the plan is creating, updating, or reusing items +- Whether web roles, table permissions, or site settings are involved +- The actual output path returned by the render script +- A note that the browser-opened HTML contains the full details + +Do not restate the per-server-logic breakdown, rationale, role assignments, or function details inline in the CLI unless the user explicitly asks for a text version. Tell the user where the detailed HTML plan file was saved, that it has been opened in the browser for review, and that the repo copy of the plan will be committed with the implementation artifacts unless the user asks to discard it. + +### 4.4 Confirm with User + +Use `AskUserQuestion`: + +| Question | Options | +|----------|---------| +| Here's the implementation plan for this server logic work. Does it look correct? | Approve and implement (Recommended), Request changes, Cancel | + +**If "Request changes"**: Ask what needs to change, update the plan, and present again. + +**If "Cancel"**: Stop the workflow. + +**Output**: User-approved implementation plan + +--- + +## Phase 5: Implement Server Logic + +**Goal**: Create each approved server logic `.js` file and metadata YAML following the constraints verified in Phase 3 + +**Actions**: + +### 5.1 Create Server Logic Folder + +For each approved server logic item: + +- **If the approved plan says `reuse`**: Do not create a new folder. Reuse the existing server logic as-is and only update the surrounding integration work if needed. +- **If the approved plan says `update` / extend**: Reuse the existing folder and update the existing `.js` / `.serverlogic.yml` files rather than creating duplicates. +- **If the approved plan says `create`**: Create the folder inside `.powerpages-site/server-logic/` (note: singular `server-logic`, no trailing 's'). Ensure the folder name matches that endpoint name exactly. + +### 5.2 Read or Create Web Roles + +Use the **Create Web Role** skill to determine which web roles are required for the approved server logic plan and to create any missing roles before writing metadata. + +Do **not** assume every server logic should get every available role. Instead, determine the minimum set of roles required for each server logic based on its purpose, security model, and the approved plan. + +Example web role file content: +```yaml +adx_anonymoususersrole: false +adx_authenticatedusersrole: true +description: Role for authenticated users +id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 +name: Authenticated Users +``` + +In the skill workflow, explicitly invoke the **Create Web Role** skill when: + +- The site has no suitable existing web roles +- The approved plan includes proposed roles that do not exist yet +- The role assignments need to be refined before metadata can be created + +After the Create Web Role skill completes, read the resulting web role YAML files and collect the `id` and `name` values needed for each server logic's metadata YAML. + +### 5.3 Create the Server Logic File + +Repeat this step for each approved server logic item. Create or update `/.powerpages-site/server-logic//.js` according to the approved plan status (`create`, `update`, or `reuse`) and follow these mandatory patterns: + +#### Structure Rules + +1. **Only top-level functions**: The file can only contain these 5 functions at the top level: `get`, `post`, `put`, `patch`, `del`. Only include the functions the user needs. +2. **Each function returns a string**: Use `JSON.stringify()` for objects/arrays. +3. **Each function has try/catch**: Every function must wrap its logic in a try/catch block. +4. **Each function logs**: Use `Server.Logger.Log()` at entry and `Server.Logger.Error()` in catch blocks. +5. **No imports or requires**: No `import`, `require`, or external dependencies. +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. + +#### Code Template + +```javascript +// Server Logic: +// Purpose: +// API URL: https:///_api/serverlogics/ + +function get() { + try { + Server.Logger.Log(" GET called"); + + // Access query parameters + // const id = Server.Context.QueryParameters["id"]; + + // Your logic here... + + return JSON.stringify({ + status: "success", + method: "GET", + data: null // replace with actual data + }); + } catch (err) { + Server.Logger.Error(" GET failed: " + err.message); + return JSON.stringify({ + status: "error", + method: "GET", + message: err.message + }); + } +} +``` + +#### Validate-and-Execute Template + +When a server logic item is identified as validate-and-execute (see Phase 2.1.1), use this pattern. The key difference: the server logic reads the current state, validates the business rule, AND writes the result to Dataverse — all in one call. The client never writes the protected field directly. + +```javascript +// Server Logic: +// Purpose: Validate and execute +// Pattern: Validate-and-execute — this endpoint both validates the business rule +// and performs the Dataverse write. The client should NOT write +// via Web API — all writes to those fields go through this endpoint. +// API URL: https:///_api/serverlogics/ + +function post() { + try { + Server.Logger.Log(" POST called"); + + const body = JSON.parse(Server.Context.Body); + const entityId = body.entityId; + const targetStatus = body.targetStatus; + + // 1. Read the current record from Dataverse + const current = Server.Connector.Dataverse.RetrieveRecord("", entityId, "?$select="); + const currentStatus = current[""]; + + // 2. Validate the transition + const allowedTransitions = { + "Draft": ["Submitted"], + "Submitted": ["Approved", "Rejected"], + "Approved": ["Fulfilled"] + }; + + const allowed = allowedTransitions[currentStatus] || []; + if (!allowed.includes(targetStatus)) { + return JSON.stringify({ + status: "error", + message: "Invalid transition: " + currentStatus + " → " + targetStatus + " is not allowed", + currentStatus: currentStatus, + targetStatus: targetStatus, + allowedTargets: allowed + }); + } + + // 3. Execute the write — server performs the Dataverse update + const updateData = {}; + updateData[""] = targetStatus; + Server.Connector.Dataverse.UpdateRecord("", entityId, JSON.stringify(updateData)); + + Server.Logger.Log(" transition executed: " + currentStatus + " → " + targetStatus); + + // 4. Return the result — client receives confirmation, not a validation flag + return JSON.stringify({ + status: "success", + previousStatus: currentStatus, + newStatus: targetStatus, + entityId: entityId + }); + } catch (err) { + Server.Logger.Error(" POST failed: " + err.message); + return JSON.stringify({ + status: "error", + message: err.message + }); + } +} +``` + +**Key differences from the basic template:** +1. The server logic reads the current state from Dataverse (not trusting the client's view) +2. It validates the business rule server-side +3. It writes the result to Dataverse via `Server.Connector.Dataverse.UpdateRecord` +4. It returns a success/failure result — NOT a `{ valid: true/false }` flag for the client to act on +5. The client calls this one endpoint — it does NOT make a separate Web API PATCH call + +#### Custom Action Wrapping Template + +When a server logic item wraps a Dataverse custom action (mapped in Phase 2.1.2), use this pattern with `Server.Connector.Dataverse.InvokeCustomApi`. The server logic acts as a portal-friendly wrapper, exposing the custom action through a `/_api/serverlogics/` endpoint with proper web role authorization. + +**Unbound action:** + +```javascript +// Server Logic: +// Purpose: Wraps Dataverse custom action "" for portal consumption +// Custom Action: (unbound, action) +// API URL: https:///_api/serverlogics/ + +function post() { + try { + Server.Logger.Log(" POST called — invoking custom action "); + + const body = JSON.parse(Server.Context.Body); + + // Build the request payload matching the custom action's input parameters + const payload = JSON.stringify({ + // "": body. + }); + + const result = Server.Connector.Dataverse.InvokeCustomApi( + "POST", + "", + payload + ); + + Server.Logger.Log(" custom action completed successfully"); + + return JSON.stringify({ + status: "success", + data: result + }); + } catch (err) { + Server.Logger.Error(" POST failed: " + err.message); + return JSON.stringify({ + status: "error", + message: err.message + }); + } +} +``` + +**Entity-bound action:** + +```javascript +function post() { + try { + Server.Logger.Log(" POST called — invoking bound action "); + + const body = JSON.parse(Server.Context.Body); + const entityId = body.entityId; + + const payload = JSON.stringify({ + // "": body. + }); + + // Include the entity set and record ID, followed by the fully qualified action name + const result = Server.Connector.Dataverse.InvokeCustomApi( + "POST", + "(" + entityId + ")/Microsoft.Dynamics.CRM.", + payload + ); + + Server.Logger.Log(" bound action completed for entity " + entityId); + + return JSON.stringify({ + status: "success", + data: result, + entityId: entityId + }); + } catch (err) { + Server.Logger.Error(" POST failed: " + err.message); + return JSON.stringify({ + status: "error", + message: err.message + }); + } +} +``` + +**Unbound function (read-only, GET):** + +```javascript +function get() { + try { + Server.Logger.Log(" GET called — invoking custom function "); + + // Pass parameters as query string for functions + const param1 = Server.Context.QueryParameters["param1"]; + const queryString = "(Param1='" + param1 + "')"; + + const result = Server.Connector.Dataverse.InvokeCustomApi( + "GET", + queryString, + null + ); + + Server.Logger.Log(" custom function completed successfully"); + + return JSON.stringify({ + status: "success", + data: result + }); + } catch (err) { + Server.Logger.Error(" GET failed: " + err.message); + return JSON.stringify({ + status: "error", + message: err.message + }); + } +} +``` + +**Key points:** +- **Unbound actions**: Use the action name as the URL, pass parameters as JSON body +- **Entity-bound actions**: Include the entity set and record ID in the URL path, followed by `Microsoft.Dynamics.CRM.` +- **Functions (GET)**: Use `"GET"` as the HTTP method and pass parameters inline in the URL using OData function call syntax +- **Actions (POST)**: Use `"POST"` as the HTTP method and pass parameters as JSON body payload +- `InvokeCustomApi` is **synchronous** — do NOT use `async`/`await` +- The server logic can add additional validation, transformation, or logging around the custom action call — it doesn't have to be a pass-through +- When Custom API response properties are known (from Phase 2.1.2), map them to the response object for clarity + +#### Referencing Secrets in Code + +When the server logic needs a secret value identified in Phase 2.3, **never hardcode the value**. Instead, read it at runtime from a site setting backed by an environment variable: + +```javascript +const apiKey = Server.Sitesetting.Get("ExternalApi/ExchangeRateApiKey"); +``` + +Use the site setting name planned in Phase 2.3. The actual environment variable and site setting YAML will be created in Phase 7. + +#### SDK Usage Guidance + +Do **not** duplicate Microsoft Learn SDK usage patterns inline in this skill. Use the documentation fetched in Phase 3 as the source of truth for connector methods, signatures, and supported patterns, then apply only the task-specific notes that were captured in the approved plan. + +### 5.4 Create the Metadata YAML + +For each approved server logic item where the plan status is `create`, generate the metadata file with the deterministic writer script instead of hand-authoring the YAML. The script generates the UUID, writes the fields in the correct order, and returns the created file path as JSON. **Skip this step for `update` / `reuse` items** — the YAML already exists and should be updated manually if needed. + +```powershell +node "${CLAUDE_PLUGIN_ROOT}/skills/add-server-logic/scripts/create-serverlogic-metadata.js" --projectRoot "" --name "" --displayName "" --description "" --webRoleIds "" +``` + +The generated `/.powerpages-site/server-logic//.serverlogic.yml` file has this structure: + +```yaml +adx_serverlogic_adx_webrole: + - + - + - +description: +display_name: +id: +name: +``` + +**Critical requirements:** + +- **`id` field is mandatory** — The script generates a new UUID (v4). PAC CLI crashes with `Expected Guid for primary key 'id'` if this is missing. +- **`adx_serverlogic_adx_webrole`** — Array of web role GUIDs from step 5.2. Include only the roles required for that server logic item. +- **`name`** — Must match the folder name and `.js` file name (the URL-friendly name used in `/_api/serverlogics/`). +- **`display_name`** — Human-readable name (e.g., "Exchange Rate API", "Order Processor"). +- **Alphabetical field ordering** — Fields must be sorted alphabetically: `adx_serverlogic_adx_webrole`, `description`, `display_name`, `id`, `name`. + +### 5.5 Validate the Code + +Before saving, verify the code against these constraints: + +| Constraint | Check | +|-----------|-------| +| Only allowed top-level functions | No functions other than get, post, put, patch, del | +| Every function returns a string | All code paths return a string (including catch blocks) | +| try/catch in every function | Every function body is wrapped in try/catch | +| Server.Logger in every function | Log at entry, Error in catch | +| No external dependencies | No `import`, `require`, `module.exports` | +| No browser APIs | No `fetch`, `XMLHttpRequest`, `setTimeout`, `console.log`, `document`, `window` | +| Async only when needed | Only functions using `await` are marked `async` | +| ECMAScript 2023 compliant | Standard JS features only (optional chaining, nullish coalescing, etc. are fine) | + +### 5.6 Git Commit + +After creating the approved server logic files, do a git commit for the server logic changes. + +If the HTML plan was generated inside the project, include it in the same commit (use the actual output path from the render script's JSON response). + +**Output**: Server logic `.js` and `.serverlogic.yml` files created, validated, and committed + +--- + +## Phase 6: Configure Table Permissions (Conditional) + +**Goal**: Set up table permissions for Dataverse tables accessed by `Server.Connector.Dataverse` in the server logic code + +**This phase only runs when the server logic uses `Server.Connector.Dataverse`.** If the server logic only uses `Server.Connector.HttpClient` (external APIs) or doesn't access Dataverse at all, skip this phase entirely and proceed to Phase 7. + +`Server.Connector.Dataverse` does **NOT** bypass table permissions — it respects them. Without table permissions configured, the Dataverse connector silently returns 0 records instead of the actual data. This is a common source of confusion. + +**Actions**: + +### 6.1 Detect Dataverse Tables and Required Privileges + +Parse the server logic `.js` file created in Phase 5 to identify which Dataverse tables are accessed and what CRUD operations are performed: + +| Dataverse SDK Method | Required Table Permission | +|---------------------|--------------------------| +| `RetrieveMultipleRecords("tablename", ...)` | Read | +| `RetrieveRecord("tablename", ...)` | Read | +| `CreateRecord("tablename", ...)` | Create | +| `UpdateRecord("tablename", ...)` | Write | +| `DeleteRecord("tablename", ...)` | Delete | + +Extract the entity set name (first argument) from each method call. Build a mapping: + +| Table (entity set name) | Read | Create | Write | Delete | +|------------------------|:----:|:------:|:-----:|:------:| +| `accounts` | Yes | — | — | — | +| `contacts` | Yes | Yes | — | — | + +### 6.2 Use the Table Permissions Architect + +When any approved server logic item uses `Server.Connector.Dataverse`, invoke the `table-permissions-architect` agent at `${CLAUDE_PLUGIN_ROOT}/agents/table-permissions-architect.md` to determine and create the required table permissions. + +**Prompt:** + +> "Analyze this Power Pages code site and propose table permissions for Dataverse tables accessed by the approved server logic plan. The following tables need permissions: +> +> [list each table with required CRUD privileges from step 6.1, grouped by server logic item] +> +> Context: +> - These permissions are needed because the server logic uses `Server.Connector.Dataverse`, which respects table permissions — without them, the connector silently returns 0 records. +> - The scope should typically be **Global** for server logic that fetches all records, unless the server logic filters by the current user (in which case use **Contact** scope). +> - The web roles assigned to these server logic items are: [list web role names and GUIDs from Phase 5.2] +> - Project root: [path] +> +> Check for existing table permissions and web roles. If new web roles are needed, create them using the create-web-role.js script. Propose a plan, then after approval create the table permission YAML files using the deterministic scripts." + +The agent will: +1. Discover existing table permissions and web roles +2. Create any missing web roles via `create-web-role.js` if needed +3. Propose a table permissions plan (with HTML visualization) +4. Present the plan via plan mode for user approval +5. After approval, create table permission YAML files in `.powerpages-site/table-permissions/` using `create-table-permission.js` + +### 6.3 Git Commit + +After table permissions (and any new web roles) are created, do a git commit for the table permissions changes. + +**Output**: Table permissions (and web roles if created) configured for all Dataverse tables accessed by the server logic + +--- + +## Phase 7: Manage Secrets & Environment Variables + +**Goal**: Securely store any secret values (API keys, client secrets, connection strings) required by the server logic as environment variables in Dataverse, optionally backed by Azure Key Vault + +**This phase only runs when the server logic requires secret or sensitive configuration values** (identified in Phase 2.3). If no secrets are needed, skip this phase and proceed to Phase 8. + +**Actions**: + +### 7.1 Recall Key Vault Decision + +The user already chose whether to use Azure Key Vault in Phase 2.3.1 (before the plan was presented). Use that decision here — do **not** re-ask. + +### 7.2a Azure Key Vault Path + +If the user chose Azure Key Vault in Phase 2.3.1: + +**Step 1 — List available Key Vaults:** + +```powershell +node "${CLAUDE_PLUGIN_ROOT}/scripts/list-azure-keyvaults.js" +``` + +The script outputs a JSON array of Key Vaults (`name`, `resourceGroup`, `location`) from the user's Azure subscription. + +**Step 2 — Select or create a Key Vault:** + +If Key Vaults were found, present the list and ask which one to use: + +Use `AskUserQuestion`: + +| Question | Context | +|----------|---------| +| Which Azure Key Vault would you like to use for storing secrets? | Present the names from the script output | + +If **no Key Vaults are found**, ask the user how to proceed: + +Use `AskUserQuestion`: + +| Question | Options | +|----------|---------| +| No Azure Key Vaults were found in your subscription. Would you like to create one, or fall back to storing secrets directly as environment variables? | Create a new Key Vault (Recommended), Store directly as environment variable | + +**If "Create a new Key Vault"**: Ask for a vault name, resource group, and location, then create it: + +Use `AskUserQuestion`: + +| Question | Context | +|----------|---------| +| What name, resource group, and Azure region would you like for the new Key Vault? | Vault names must be 3-24 characters, globally unique, start with a letter, and contain only alphanumerics and hyphens. Suggest a name based on the project/site name. | + +```powershell +node "${CLAUDE_PLUGIN_ROOT}/scripts/create-azure-keyvault.js" \ + --name "" \ + --resourceGroup "" \ + --location "" +``` + +The script outputs a JSON object with `name`, `resourceGroup`, and `location`. Use the created vault for the remaining steps. + +**If "Store directly as environment variable"**: Skip the rest of Phase 7.2a and proceed to Phase 7.2b (direct environment variable path). + +**Step 3 — Provide instructions for storing each secret in Key Vault:** + +For each secret identified in Phase 2.3, give the user instructions to store the value themselves. Do **not** ask for the secret value — secret values must never pass through the conversation. + +Present **both** methods (CLI and Azure Portal) so the user can choose whichever they prefer: + +**Option A — Azure CLI (recommended for automation):** + +Present the commands as a numbered list the user can copy and run. Use the stdin form so the secret value does not appear in process listings: + +``` +For each secret, run the following command (replacing with the actual value): + +1. : + printf '%s' '' | node "${CLAUDE_PLUGIN_ROOT}/scripts/store-keyvault-secret.js" \ + --vaultName "" \ + --secretName "" +``` + +Tell the user each command outputs a JSON object with a `secretUri` and to share the output (which contains only the URI, not the secret) so the workflow can continue. + +**Option B — Azure Portal:** + +Provide these steps for each secret: + +``` +1. Go to the Azure Portal (https://portal.azure.com) +2. Search for "Key vaults" in the top search bar and select it +3. Select the Key Vault: +4. In the left menu under "Objects", click "Secrets" +5. Click "+ Generate/Import" at the top +6. Fill in the fields: + - Upload options: Manual + - Name: + - Secret value: paste your secret value here + - Leave other fields as defaults +7. Click "Create" +8. After creation, click on the secret name, then click the current version +9. Copy the "Secret Identifier" URI and share it here so the workflow can continue +``` + +Tell the user the Secret Identifier URI looks like `https://.vault.azure.net/secrets//` and that this URI (not the secret value) is what should be shared back. + +**Step 4 — Create environment variable in Dataverse:** + +After the user shares the `secretUri` output from each command, create an environment variable definition in Dataverse that references the Key Vault secret. Use the `secret` type: + +```powershell +node "${CLAUDE_PLUGIN_ROOT}/scripts/create-environment-variable.js" "" \ + --schemaName "" \ + --displayName "" \ + --type "secret" \ + --value "" +``` + +**Step 5 — Create site setting for the environment variable:** + +For each environment variable, create a site setting YAML that maps to it: + +```powershell +node "${CLAUDE_PLUGIN_ROOT}/scripts/create-site-setting.js" \ + --projectRoot "" \ + --name "" \ + --envVarSchema "" +``` + +This creates a site setting with `envvar_schema` and `source: 1`, which tells Power Pages to resolve the value from the Dataverse environment variable (backed by Key Vault). + +### 7.2b Direct Environment Variable Path + +If the user chose not to use Azure Key Vault: + +**Step 1 — Create environment variables with placeholder values:** + +For each secret identified in Phase 2.3, create the environment variable in Dataverse with a placeholder value: + +```powershell +node "${CLAUDE_PLUGIN_ROOT}/scripts/create-environment-variable.js" "" \ + --schemaName "" \ + --displayName "" \ + --value "PLACEHOLDER_SET_ACTUAL_VALUE" +``` + +**Step 2 — Create site setting for the environment variable:** + +```powershell +node "${CLAUDE_PLUGIN_ROOT}/scripts/create-site-setting.js" \ + --projectRoot "" \ + --name "" \ + --envVarSchema "" +``` + +**Step 3 — Give the user steps to set the actual secret values:** + +Do **not** ask for secret values — they must never pass through the conversation. Instead, tell the user to update each placeholder with the real value using one of these approaches: + +1. **Power Apps maker portal** ([make.powerapps.com](https://make.powerapps.com)) — Go to **Solutions** → **Default Solution** → **Environment variables** → find the variable by display name → update the value + +Present the list of environment variables that need updating (display name and schema name for each) so the user knows exactly which ones to set. + +### 7.3 Verify Environment Variable Configuration + +After creating all environment variables and site settings: + +- Confirm each site setting YAML was created in `.powerpages-site/site-settings/` +- Verify each YAML contains `envvar_schema` and `source: 1` +- Confirm the server logic code references the correct site setting names via `Server.Sitesetting.Get("")` + +### 7.4 Git Commit + +Do a git commit for the environment variable site setting changes. + +**Output**: Environment variables created in Dataverse (with or without Azure Key Vault backing), site settings configured, server logic referencing correct setting names + +--- + +## Phase 8: Configure Site Settings + +**Goal**: Set up site settings for the server logic feature + +**Actions**: + +### 8.1 Configure Server Logic Site Settings + +The `.powerpages-site` folder is guaranteed to exist at this point (verified in Phase 1.5). + +The following site settings control server logic behavior. Only create settings that differ from defaults or are specifically needed: + +| Setting | Description | Default | When to configure | +|---------|-------------|---------|-------------------| +| `ServerLogic/Enabled` | Enable/disable server logic feature | `true` | Only if explicitly disabled and needs re-enabling | +| `ServerLogic/AllowedDomains` | Restrict which external domains HttpClient can call | All domains | When the server logic calls external APIs and you want to restrict to specific domains for security | +| `ServerLogic/TimeoutInSeconds` | Maximum execution time | `120` | The platform caps this at **120 seconds** — values above 120 are silently clamped. Only configure when you need to lower the timeout, not raise it. | +| `ServerLogic/AllowNetworkingToAllDomains` | Allow networking across domains | `true` | Set to `false` when restricting via AllowedDomains | + +Use the existing site setting creation script: + +```powershell +node "${CLAUDE_PLUGIN_ROOT}/scripts/create-site-setting.js" --projectRoot "" --name "ServerLogic/AllowedDomains" --value "api.example.com,api.other.com" --description "Restrict server logic external API calls to these domains" +``` + +### 8.2 Git Commit + +If any settings were created: + +Do a git commit for the site settings changes. + +**Output**: Site settings configured and committed (or skipped if not needed/deployed) + +--- + +## Phase 9: Client-Side Integration + +**Goal**: Help the user call the server logic endpoints from their site's frontend code, matching existing patterns discovered in Phase 1 + +Server logic creates the backend — but without frontend code to call it, the endpoints are unused. This phase creates or updates frontend code to consume the server logic APIs, using the patterns and conventions already established in the codebase. + +**Actions**: + +### 9.1 Ask User About Integration Scope + +Use `AskUserQuestion`: + +| Question | Options | +|----------|---------| +| I've created the server logic backend. Would you like me to also fully integrate it into the frontend UI? | Yes, fully integrate it into the UI (Recommended), No, I'll handle the frontend myself | + +**If "No"**: Skip to Phase 10, but provide the API URL and a code snippet the user can copy. + +### 9.2 Follow the Frontend Integration Reference + +Use the reference below for the frontend integration approach, examples, and framework-specific patterns: + +> Reference: `${CLAUDE_PLUGIN_ROOT}/skills/add-server-logic/references/frontend-integration-reference.md` + +Based on the Explore agent's findings from Phase 1.4 and the approved plan, choose the integration approach from that reference and apply it consistently across all server logic endpoints being wired into the frontend. + +### 9.3 Create or Update Frontend Integration + +Following the reference: + +- Reuse the existing service layer or API utility when the site already has one +- Create a lightweight CSRF-aware helper only when the site has no established API client pattern +- Group multiple server logic endpoints into a coherent service module when that improves maintainability +- Add framework-specific hooks/composables/services only when the codebase already follows that pattern +- Fully integrate the server logic into the actual UI flow — do **not** stop at creating service/helper code +- Update the relevant pages, components, forms, buttons, or user journeys so the new backend behavior is reachable from the interface +- Replace placeholder data, mock handlers, or temporary actions when they are meant to be backed by the new server logic endpoints +- Add or preserve loading, success, empty, and error states so the UI behaves like a finished feature +- **For validate-and-execute endpoints**: The frontend must call the server logic endpoint for the protected operation (e.g., status transition) — it must NOT make a separate Web API PATCH for the same field. Ensure the UI for that operation (e.g., a "Submit" or "Approve" button) calls the server logic service function, not the Web API service + +### 9.4 Git Commit + +If frontend integration code was created: + +Do a git commit for the frontend integration changes. + +**Output**: Frontend service/hook created as needed, UI components/pages fully integrated, and changes committed + +--- + +## Phase 10: Verify & Test Guidance + +**Goal**: Validate the code and provide the user with everything needed to test the server logic + +**Actions**: + +### 10.1 Final Code Validation + +Re-read each created `.js` file and verify: + +- [ ] Only allowed top-level functions (get, post, put, patch, del) +- [ ] Every function returns a string +- [ ] try/catch in every function +- [ ] Server.Logger calls in every function +- [ ] No `import`, `require`, or external dependencies +- [ ] No browser APIs (`fetch`, `XMLHttpRequest`, `setTimeout`, `console.log`, `document`, `window`) +- [ ] Async only on functions that use await +- [ ] Correct SDK method usage (verified against Phase 3 documentation) +- [ ] HttpClient used only for external APIs (not Dataverse) +- [ ] Dataverse connector used for Dataverse operations + +Re-read each `.serverlogic.yml` file and verify: + +- [ ] `id` field exists and is a valid UUID +- [ ] `adx_serverlogic_adx_webrole` array is non-empty (at least one web role) +- [ ] `name` matches the folder name and `.js` file name +- [ ] `display_name` and `description` are populated +- [ ] Fields are alphabetically sorted +- [ ] File names match: folder name, `.js` name, `.serverlogic.yml` name, and `name` field all use the same value + +### 10.2 Provide API URL + +Tell the user each endpoint URL: + +``` +https:///_api/serverlogics/ +``` + +### 10.3 Test Guidance + +Provide testing instructions: + +1. **Deploy the site first** — The server logic must be deployed via `/deploy-site` before it can be called +2. **CSRF token required for non-GET requests** — POST, PUT, PATCH, and DELETE calls to server logic endpoints require a CSRF token. Fetch the token from `/_layout/tokenhtml` and include it as `__RequestVerificationToken` header. GET requests are **exempt** from antiforgery validation — no token is needed for read-only calls. +3. **Authentication** — Server logic respects the site's authentication. Calls from authenticated sessions use cookie-based auth automatically. Anonymous access depends on governance settings. +4. **Testing from browser console**: + +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 + +**Output**: Code validated, API URL provided, test guidance given + +--- + +## Phase 11: Review & Deploy + +**Goal**: Present a summary of all work performed and offer deployment + +**Actions**: + +### 11.1 Record Skill Usage + +> Reference: `${CLAUDE_PLUGIN_ROOT}/references/skill-tracking-reference.md` + +Follow the skill tracking instructions in the reference to record this skill's usage. Use `--skillName "AddServerLogic"`. + +### 11.2 Present Summary + +Present a summary of everything that was done: + +| Step | Status | Details | +|------|--------|---------| +| Server Logic JS | Created | List each created `.powerpages-site/server-logic//.js` file | +| Server Logic YAML | Created | List each created `.powerpages-site/server-logic//.serverlogic.yml` file | +| HTML Plan | Created/Updated | Actual path from render script output | +| Functions | Implemented | Summarize methods implemented per server logic item | +| SDK Features Used | — | Summarize features used per server logic item | +| Table Permissions | Created/Skipped | `accounts` (Read), `contacts` (Read, Create), etc. | +| Secrets & Env Vars | Created/Skipped | Environment variables (Key Vault-backed or direct), site settings with `envvar_schema` | +| Site Settings | Created/Skipped | ServerLogic/AllowedDomains, etc. | +| Client-Side Service | Created/Skipped | List created or updated frontend service files | +| UI Integration | Created/Skipped | Pages, components, forms, or actions fully wired to the server logic endpoints | +| API URL | — | List each `/_api/serverlogics/` URL | + +### 11.3 Ask to Deploy + +Use `AskUserQuestion`: + +| Question | Options | +|----------|---------| +| The server logic work is ready. To make it live, the site needs to be deployed. Would you like to deploy now? | Yes, deploy now (Recommended), No, I'll deploy later | + +**If "Yes, deploy now"**: Invoke the `/deploy-site` skill to deploy the site. + +After deployment succeeds, use `AskUserQuestion`: + +| Question | Options | +|----------|---------| +| The site has been deployed. Would you like me to run `/test-site` to validate it now? | Yes, run `/test-site` (Recommended), No, skip testing | + +**If "Yes, run `/test-site`"**: Invoke the `/test-site` skill. + +**If "No, I'll deploy later"**: Acknowledge and remind: + +> "No problem! Remember to deploy your site using `/deploy-site` when you're ready. The server logic endpoints won't be accessible until the site is deployed." + +### 11.4 Post-Deploy Notes + +After deployment (or if skipped), remind the user: + +- **Test the endpoints**: Call each `/_api/serverlogics/` URL with the appropriate HTTP method (include CSRF token for non-GET requests) +- **Recommended full-site validation**: After deployment, ask whether to run `/test-site` so the live site can be validated end to end +- **Check logs**: Use Server.Logger output in Power Pages design studio diagnostics to debug issues +- **Table permissions**: Table permissions were configured for Dataverse tables used by this server logic. If you add new Dataverse tables later, run the table permissions setup again — without permissions, `Server.Connector.Dataverse` silently returns 0 records +- **Timeout**: Default execution timeout is 120 seconds — this is also the platform maximum (values above 120 are silently clamped) +- **Anonymous access**: If the site's governance control disables anonymous access, anonymous users cannot invoke server logic that integrates with external systems +- **Preview feature**: Server Logic is currently in preview — monitor Microsoft Learn for updates +- **Environment variables with placeholder values**: If Phase 7 created environment variables with placeholder values, remind the user to update them with the actual secret values before testing. They can do this via: + 1. **Power Platform admin center** — **Environments** → select environment → **Environment variables** → find by display name → update value + 2. **Power Apps maker portal** — **Solutions** → open solution → **Environment variables** → edit value + +**Output**: Summary presented, deployment completed or deferred, post-deploy guidance provided + +--- + +## Important Notes + +### Throughout All Phases + +- **Use TaskCreate/TaskUpdate** to track progress at every phase +- **Always fetch Microsoft Learn docs** in Phase 3 before writing code — the docs are the source of truth +- **Ask for user confirmation** at key decision points +- **Commit at milestones** — after server logic code, table permissions (if any), secrets/environment variables (if any), site settings, and frontend integration (if any) +- **Validate thoroughly** — server logic has strict constraints and violations cause runtime errors + +### Key Decision Points (Wait for User) + +1. At Phase 1.5: Deploy now or cancel (if `.powerpages-site` missing — mandatory) +2. At Phase 2.1.2: Use existing Dataverse custom actions or build from scratch (if custom actions found) +3. At Phase 2: Confirm requirements (purpose, name, HTTP methods, secrets) +4. At Phase 4: Approve implementation plan before writing code +5. At Phase 6.2: Review and approve the `table-permissions-architect` plan (if Dataverse connector is used) +6. At Phase 2.3.1: Choose Azure Key Vault or direct environment variable (if secrets needed) +7. At Phase 7.2a Step 2: Create a new Key Vault or fall back to direct environment variable (if no vaults found) +8. At Phase 9.1: Create frontend integration or skip +9. At Phase 11.3: Deploy now or deploy later + +### SDK Pattern Source of Truth + +Do not treat this skill file as the canonical SDK reference. The Phase 3 Microsoft Learn fetch is the source of truth for SDK usage patterns, supported methods, signatures, and connector behavior. Keep only task-specific decisions in the plan and implementation notes. + +### Progress Tracking + +Before starting Phase 1, create a task list with all phases using `TaskCreate`: + +| Task subject | activeForm | Description | +|-------------|------------|-------------| +| Verify site exists | Verifying site prerequisites | Locate project root, detect framework, explore existing server logics and frontend patterns, verify .powerpages-site exists (mandatory) | +| Understand requirements | Gathering requirements | Determine user intent, whether one or more server logic files are needed, the methods/features for each item, discover Dataverse custom actions, and any secrets required | +| Fetch latest documentation | Fetching Microsoft Learn docs | Query Microsoft Learn for current Server Logic SDK reference and samples | +| Review implementation plan | Reviewing plan with user | Present plan (server logic inventory, functions, SDK features, external APIs, secrets) and confirm before writing code | +| Implement server logic | Writing server logic code | Determine/create required web roles, create approved `.js` and `.serverlogic.yml` files, validate code | +| Configure table permissions | Setting up Dataverse table permissions | (Conditional) Parse `.js` files for Dataverse tables, launch `table-permissions-architect`, create permission YAML files | +| Manage secrets and environment variables | Configuring secrets and env vars | (Conditional) Recommend Azure Key Vault, list vaults, store secrets, create environment variables in Dataverse, create site settings with envvar_schema | +| Configure site settings | Configuring site settings | Set up ServerLogic/* site settings if needed | +| Client-side integration | Wiring frontend to server logic | Follow the frontend integration reference, create/update service files as needed, and fully wire the UI to the server logic endpoints | +| Verify and test guidance | Validating and providing test guidance | Final validation, API URLs, CSRF token instructions, testing guide | +| Review and deploy | Reviewing summary and deploying | Present summary, ask about deployment, provide post-deploy guidance | + +Mark each task `in_progress` when starting it and `completed` when done via `TaskUpdate`. Use `TaskList` between phase transitions and before the final summary to confirm there are no incomplete work items left. + +--- + +**Begin with Phase 1: Verify Site Exists** diff --git a/plugins/power-pages/skills/add-server-logic/assets/serverlogic-plan.html b/plugins/power-pages/skills/add-server-logic/assets/serverlogic-plan.html new file mode 100644 index 000000000..9c5d6a639 --- /dev/null +++ b/plugins/power-pages/skills/add-server-logic/assets/serverlogic-plan.html @@ -0,0 +1,453 @@ + + + + + +__PLAN_TITLE__ - __SITE_NAME__ + + + + +
+
+
+ +
+
__PLAN_TITLE__
+
__SITE_NAME__
+
+
+
+
+ +
+ + +
+
+

Plan Overview

+

Server logic implementation plan for __SITE_NAME__

+ +
__SUMMARY__
+ +
+
0
Server Logic Items
+
0
New / Create
+
0
Reused
+
+ +
+ +
+
Status Summary
+
+
+ +

Design Rationale

+
+
+ +
+

Web Roles

+

+
+
+ +
+

Server Logic

+

+
+
+
+
+
+ + +
AI-generated content may be incorrect
+ + diff --git a/plugins/power-pages/skills/add-server-logic/references/frontend-integration-reference.md b/plugins/power-pages/skills/add-server-logic/references/frontend-integration-reference.md new file mode 100644 index 000000000..d03da66aa --- /dev/null +++ b/plugins/power-pages/skills/add-server-logic/references/frontend-integration-reference.md @@ -0,0 +1,202 @@ +# Frontend Integration Reference + +Use this reference in Phase 9 of `add-server-logic` to decide how the site's frontend should call one or more server logic endpoints. + +## Goal + +Choose the lightest integration approach that matches the existing codebase patterns. Reuse established utilities when possible. Only introduce new helpers when the site does not already have a consistent API calling pattern. + +Frontend integration is **not complete** when only a helper or service file exists. The endpoint must be wired into the actual user experience unless the user explicitly asks for backend-only work. + +## Decision Order + +1. Reuse an existing service layer or API wrapper if one already exists. +2. Reuse existing CSRF token handling patterns if the site already has them. +3. Create a new helper only when no suitable pattern exists. +4. Group related server logic endpoints into a coherent service module when multiple endpoints are being introduced together. +5. Add framework-specific hooks/composables/services only when the codebase already uses those abstractions. + +## Existing Pattern Detection + +Look for: + +- `shell.safeAjax` usage in legacy or jQuery-based sites +- Shared fetch wrappers such as `powerPagesApi.ts`, `apiClient.ts`, or framework-specific service modules +- Existing CSRF token helpers built around `/_layout/tokenhtml` +- Existing hooks/composables/services that wrap backend calls with loading and error state + +## Server Logic Response Envelope + +Server logic endpoints return responses in a standard JSON envelope: + +```json +{ + "requestId": "", + "success": true, + "serverLogicName": "", + "data": "", + "error": null +} +``` + +- `data` contains the string returned by the invoked function (e.g., the `JSON.stringify(...)` result). Parse it with `JSON.parse(response.data)` when the function returns serialized JSON. +- On failure, `success` is `false`, `data` is `null`, and `error` contains the error message. +- `requestId` is the server-side activity GUID — useful for correlating with `Server.Logger` output in diagnostics. + +All frontend helpers and service wrappers should unwrap `.data` from this envelope rather than treating the entire response body as the function's return value. + +## Recommended Approaches + +### 1. Sites Using `shell.safeAjax` + +If the site already uses `shell.safeAjax`, create thin wrappers around it instead of introducing a new fetch abstraction. + +Use this shape: + +```javascript +function callServerLogic(method, endpointName, queryParams, body) { + return new Promise((resolve, reject) => { + let url = `/_api/serverlogics/${endpointName}`; + if (queryParams) { + url += '?' + new URLSearchParams(queryParams).toString(); + } + + shell.safeAjax({ + type: method, + url, + contentType: 'application/json', + data: body ? JSON.stringify(body) : undefined, + success: function (res) { resolve(res); }, + error: function (xhr) { reject(xhr); } + }); + }); +} +``` + +### 2. SPA Sites with an Existing API Wrapper + +If the site already has a helper such as `powerPagesFetch`, reuse it and add one or more thin server logic functions on top. + +Use this shape: + +```typescript +import { powerPagesFetch } from '../shared/powerPagesApi'; + +export async function callServerLogic( + endpointName: string, + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', + params?: Record, + body?: unknown +): Promise { + const url = params + ? `/_api/serverlogics/${endpointName}?${new URLSearchParams(params)}` + : `/_api/serverlogics/${endpointName}`; + + const envelope = await powerPagesFetch<{ data: string; success: boolean; error: string | null }>(url, { + method, + body: body ? JSON.stringify(body) : undefined, + }); + + if (!envelope.success) { + throw new Error(envelope.error ?? 'Server logic call failed'); + } + + return JSON.parse(envelope.data) as T; +} +``` + +### 3. SPA Sites Without an Existing API Wrapper + +If the site has no established API client, create a lightweight CSRF-aware helper and keep it narrowly scoped. + +Use this shape: + +```typescript +async function getCsrfToken(): Promise { + const response = await fetch('/_layout/tokenhtml'); + const html = await response.text(); + const match = html.match(/value="([^"]+)"/); + if (!match) { + throw new Error('Failed to get CSRF token'); + } + return match[1]; +} + +export async function callServerLogic( + endpointName: string, + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', + params?: Record, + body?: unknown +): Promise { + const url = params + ? `/_api/serverlogics/${endpointName}?${new URLSearchParams(params)}` + : `/_api/serverlogics/${endpointName}`; + + const headers: Record = { + 'Content-Type': 'application/json', + }; + + // CSRF token is required for non-GET requests only + if (method !== 'GET') { + headers['__RequestVerificationToken'] = await getCsrfToken(); + } + + const response = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + + if (!response.ok) { + throw new Error(`Server logic call failed: ${response.status}`); + } + + return response.json(); +} +``` + +## Multiple Server Logic Endpoints + +When a single user request results in multiple server logic endpoints: + +- Prefer one shared helper plus endpoint-specific wrapper functions +- Group related endpoints into one service module when they belong to the same feature area +- Keep endpoint names explicit rather than hiding them behind vague generic method names +- Share token handling and low-level request plumbing; keep business semantics in endpoint-specific functions + +Example: + +```typescript +export const orderServerLogic = { + getSummary: () => callServerLogic('order-summary', 'GET'), + submitOrder: (payload: unknown) => callServerLogic('order-submit', 'POST', undefined, payload), +}; +``` + +## Framework-Specific Abstractions + +Only add hooks/composables/services with loading/error state when the site already uses that pattern. + +- **React**: `useServerLogic` or feature-specific hooks such as `useOrderSummary` +- **Vue**: composables such as `useServerLogic` +- **Angular**: injectable services returning observables or promises following existing conventions +- **Astro**: plain service modules are usually sufficient + +## Component Updates + +When integrating the new endpoints into existing UI: + +- Make the feature reachable from the real UI flow — a button, form submission, page load, filter action, or other user-triggered path +- Replace mock data or placeholder URLs only when they clearly map to the approved server logic plan +- Preserve existing loading, empty, and error states when present +- Add loading/error handling if the component currently has none and the codebase pattern supports it +- Avoid broad refactors unrelated to the server logic integration + +## Output Expectations + +Phase 9 should leave behind: + +- The frontend helper or service files needed to call the server logic endpoints +- Any framework-specific wrappers that match the site's existing architecture +- Updated components/pages/forms/actions wired to the new endpoints when the scope includes that work +- A summary of which frontend files were created or changed and which endpoints they call diff --git a/plugins/power-pages/skills/add-server-logic/references/server-logic-docs.md b/plugins/power-pages/skills/add-server-logic/references/server-logic-docs.md new file mode 100644 index 000000000..528fa1a08 --- /dev/null +++ b/plugins/power-pages/skills/add-server-logic/references/server-logic-docs.md @@ -0,0 +1,98 @@ +# Server Logic Documentation Discovery + +Power Pages Server Logic is a preview feature with documentation that may expand or change at any time. **Never rely on a hardcoded list of URLs.** Always search Microsoft Learn dynamically to discover all available pages. + +## Discovery Strategy + +### Step 1: Search to discover pages + +``` +mcp__plugin_power-pages_microsoft-learn__microsoft_docs_search("Power Pages Server Logic") +``` + +### Step 2: Collect unique page URLs + +From all search results, extract unique `contentUrl` values. Keep pages that match: +- `learn.microsoft.com/.../power-pages/configure/server-logic*` +- `learn.microsoft.com/.../power-pages/configure/server-objects*` +- `learn.microsoft.com/.../power-pages/configure/author-server-logic*` + +Discard: release-plan announcements, blog posts, unrelated configuration pages. + +### Step 3: Classify and fetch + +Classify each discovered page into one of these categories: + +| Category | Always fetch? | How to identify | +|----------|:------------:|----------------| +| **Core reference** | Yes | Overview page, authoring guide, SDK/server objects reference | +| **How-to guide** | If relevant | Tutorials for specific scenarios (Dataverse, external APIs, Azure Functions, Graph, etc.) | +| **New/unknown** | If relevant | Any page not matching known patterns — read it to learn about new capabilities | + +### Step 4: Fetch in parallel + +Fetch all core reference pages plus relevant how-to guides in parallel using `mcp__plugin_power-pages_microsoft-learn__microsoft_docs_fetch`. + +## Known Pages (as of March 2026) + +These are pages that existed when this reference was last updated. They serve as a baseline — the search step above will discover these plus any new ones: + +| Page | URL | +|------|-----| +| Overview | `https://learn.microsoft.com/en-us/power-pages/configure/server-logic-overview` | +| Author server logic | `https://learn.microsoft.com/en-us/power-pages/configure/author-server-logic` | +| Server objects (SDK) | `https://learn.microsoft.com/en-us/power-pages/configure/server-objects` | +| Dataverse operations | `https://learn.microsoft.com/en-us/power-pages/configure/server-logic-operations` | +| External services | `https://learn.microsoft.com/en-us/power-pages/configure/server-logic-external-services` | +| Azure Function | `https://learn.microsoft.com/en-us/power-pages/configure/server-logic-azure-function` | +| Graph & SharePoint | `https://learn.microsoft.com/en-us/power-pages/configure/server-logic-graph-sharepoint` | + +If the search discovers pages not in this table, those are new additions — fetch and use them. + +## What to Extract from the Docs + +For the current task, capture: + +- All SDK method signatures, parameter types, and return types +- Supported HTTP methods and function signatures +- Site settings and their defaults +- Security model details (web roles, table permissions, CSRF) +- Client-side calling patterns and response formats +- Any new methods, changed behaviors, or breaking changes + +## Use-Case Mapping + +When the user's requirements are known, fetch any additional pages that match the scenario: + +| User needs | Look for pages about | +|-----------|---------------------| +| Dataverse CRUD | Dataverse operations, table interactions | +| External API calls | External services, HttpClient | +| Azure Functions | Azure Function HTTP trigger | +| Microsoft Graph / SharePoint | Graph API, SharePoint integration | +| Any other scenario | Any matching tutorial or how-to page | + +If the search results contain unfamiliar but relevant pages, read them — they may document new capabilities. + +## Code Samples + +Also search for current samples: + +``` +mcp__plugin_power-pages_microsoft-learn__microsoft_code_sample_search("Power Pages server logic") +``` + +## Known SDK Baseline + +Use this as a baseline only. If Microsoft Learn differs, Microsoft Learn wins. + +- **Server.Logger**: `Log(message)`, `Warn(message)`, `Error(message)` +- **Server.Context**: `QueryParameters["key"]`, `Headers["key"]`, `Body`, `HttpMethod`, `Url`, `ActivityId`, `FunctionName`, `ServerLogicName` +- **Server.Connector.HttpClient**: `GetAsync(url, headers?)`, `PostAsync(url, jsonBody, headers?, contentType?)`, `PatchAsync(url, jsonBody, headers?, contentType?)`, `PutAsync(url, jsonBody, headers?, contentType?)`, `DeleteAsync(url, headers?)` +- **Server.Connector.Dataverse**: `CreateRecord(entitySetName, payload)`, `RetrieveRecord(entitySetName, id, options)`, `RetrieveMultipleRecords(entitySetName, options)`, `UpdateRecord(entitySetName, id, payload)`, `DeleteRecord(entitySetName, id)`, `InvokeCustomApi(httpMethod, url, payload)` +- **Server.User**: `fullname`, `firstname`, `lastname`, `emailaddress1`, `contactid`, `Roles`, `Token`, and many other contact properties +- **Server.Website**: `adx_websiteid`, `adx_name`, `adx_primarydomainname`, `adx_defaultlanguage`, etc. +- **Server.Sitesetting**: `Get(name)` +- **Server.EnvironmentVariable**: `get(name)` — reads Dataverse environment variable values directly (alternative to reading via site settings with `envvar_schema`) + +When new SDK members or changed patterns are discovered, use them and record the differences in the implementation plan. diff --git a/plugins/power-pages/skills/add-server-logic/references/server-logic-plan-data-format.md b/plugins/power-pages/skills/add-server-logic/references/server-logic-plan-data-format.md new file mode 100644 index 000000000..b299857d1 --- /dev/null +++ b/plugins/power-pages/skills/add-server-logic/references/server-logic-plan-data-format.md @@ -0,0 +1,165 @@ +# Server Logic Plan Data Format + +Reference document for generating the HTML server logic plan using `render-serverlogic-plan.js`. + +## Determine Output Location + +- **If working in the context of a website** (a project root with `powerpages.config.json` exists): write the file to `/docs/serverlogic-plan.html` +- **Otherwise**: write to the system temp directory (`[System.IO.Path]::GetTempPath()`) + +## Prepare Data + +Write a temporary JSON data file with these keys: + +```json +{ + "SITE_NAME": "The site name from powerpages.config.json or the project folder", + "PLAN_TITLE": "A short plan title such as 'Server Logic Plan'", + "SUMMARY": "A 1-2 sentence summary of what this endpoint will do and why", + "WEB_ROLES_DATA": [], + "SERVER_LOGICS_DATA": [], + "RATIONALE_DATA": [], + "SECRETS_DATA": null +} +``` + +### WEB_ROLES_DATA Format + +```json +[ + { + "id": "role-authenticated", + "name": "Authenticated Users", + "desc": "Built-in role for signed-in users.", + "builtin": true, + "isNew": false, + "color": "#8890a4" + } +] +``` + +### SERVER_LOGICS_DATA Format + +```json +[ + { + "id": "ticket-dashboard", + "name": "ticket-dashboard", + "displayName": "Ticket Dashboard", + "status": "create", + "apiUrl": "https:///_api/serverlogics/ticket-dashboard", + "webRoles": [ + { + "id": "role-authenticated", + "reasoning": "Authenticated users need access because the dashboard is part of the signed-in support workspace." + } + ], + "rationale": "Keeps Dataverse queries and shaping logic off the client while enforcing role-scoped access.", + "functions": [ + { + "name": "get", + "purpose": "Return dashboard metrics", + "reasoning": "The dashboard is read-heavy, so GET keeps the endpoint simple and cache-friendly." + } + ] + } +] +``` + +Use `status` values like `create`, `update`, or `reuse`. + +Each `webRoles` entry should explain **why that specific role is assigned to that specific server logic**. + +Each `functions` entry should explain **why that specific function is being implemented for that specific server logic**. + +When a server logic item wraps a Dataverse custom action (mapped in Phase 2.1.2), include a `customAction` object: + +```json +{ + "id": "calculate-discount", + "name": "calculate-discount", + "displayName": "Calculate Discount", + "status": "create", + "customAction": { + "name": "new_CalculateDiscount", + "displayName": "Calculate Discount", + "type": "action", + "binding": "entity", + "boundEntity": "salesorder" + }, + "...": "other fields as above" +} +``` + +| Field | Description | +|-------|-------------| +| `customAction` | *(Optional)* Present only when the server logic wraps a Dataverse custom action | +| `customAction.name` | The unique name of the custom action (used in `InvokeCustomApi`) | +| `customAction.displayName` | Human-readable display name | +| `customAction.type` | `action` (POST) or `function` (GET) | +| `customAction.binding` | `unbound`, `entity`, or `entityCollection` | +| `customAction.boundEntity` | *(Optional)* Logical name of the bound entity, if applicable | + +When `customAction` is present, the plan HTML renders a badge on the server logic card indicating that it wraps an existing Dataverse custom action. When `customAction` is absent or `null`, no badge is shown. + +### RATIONALE_DATA Format + +```json +[ + { + "icon": "🛡️", + "title": "Why this structure", + "desc": "Separate server logic files keep responsibilities focused and make role assignment clearer." + } +] +``` + +The overview tab renders these rationale items in the same style as the other Power Pages plan documents. + +### SECRETS_DATA Format + +Set to `null` when the server logic does not require any secrets. When the user has chosen to use Azure Key Vault, provide an object: + +```json +{ + "useKeyVault": true, + "vaultName": "contoso-keyvault", + "secrets": [ + { + "name": "ExchangeRateApiKey", + "purpose": "API key for the exchange rate service", + "siteSetting": "ExternalApi/ExchangeRateApiKey", + "serverLogicId": "exchange-rate" + } + ] +} +``` + +| Field | Description | +|-------|-------------| +| `useKeyVault` | `true` if the user chose Azure Key Vault; omit or set `false` for direct env vars | +| `vaultName` | *(Optional)* Name of the selected/created Key Vault — shown in the plan if known | +| `secrets[].name` | Descriptive name for the secret (e.g., `ExchangeRateApiKey`) | +| `secrets[].purpose` | Why the secret is needed | +| `secrets[].siteSetting` | The site setting name the server logic reads via `Server.Sitesetting.Get()` | +| `secrets[].serverLogicId` | The `id` (or `name`) of the server logic item that uses this secret — links the secret to the correct card in the plan | + +When `useKeyVault` is `true`, the plan HTML renders a prominent banner in the Overview tab explaining that secrets are stored in Azure Key Vault and why it matters (centralized access control, audit logging, rotation support, secrets never in code). Each server logic card also shows the secrets it depends on. + +When `SECRETS_DATA` is `null` or `useKeyVault` is `false`, the banner and per-card secrets sections are hidden. + +## Render the HTML File + +Do **not** write the HTML manually. Use the render script: + +```powershell +node "${CLAUDE_PLUGIN_ROOT}/scripts/render-serverlogic-plan.js" --output "" --data "" +``` + +The render script refuses to overwrite existing files. Before calling it, check if the default output path (`/docs/serverlogic-plan.html`) already exists. If it does, choose a new descriptive filename based on context — e.g., `serverlogic-plan-exchange-rate.html`, `serverlogic-plan-apr-2026.html`. Pass the chosen name via `--output`. + +Delete the temporary data JSON file after the script succeeds. + +## Open in Browser + +Open the generated HTML file in the user's default browser. diff --git a/plugins/power-pages/skills/add-server-logic/scripts/create-serverlogic-metadata.js b/plugins/power-pages/skills/add-server-logic/scripts/create-serverlogic-metadata.js new file mode 100644 index 000000000..7d459ba01 --- /dev/null +++ b/plugins/power-pages/skills/add-server-logic/scripts/create-serverlogic-metadata.js @@ -0,0 +1,100 @@ +#!/usr/bin/env node + +// Creates a server logic metadata YAML file for Power Pages code sites. +// Generates UUID, validates inputs, and writes correctly-formatted YAML. +// +// Usage: +// node create-serverlogic-metadata.js --projectRoot --name --displayName --description --webRoleIds +// +// Output (JSON to stdout): +// { "id": "", "filePath": "" } +// +// Exits with code 1 on validation errors (messages to stderr). + +const fs = require('fs'); +const path = require('path'); +const generateUuid = require(path.join(__dirname, '..', '..', '..', 'scripts', 'generate-uuid')); + +const args = process.argv.slice(2); + +function getArg(name) { + const idx = args.indexOf(`--${name}`); + return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null; +} + +const projectRoot = getArg('projectRoot'); +const endpointName = getArg('name')?.trim() || null; +const displayName = getArg('displayName')?.trim() || null; +const description = getArg('description')?.trim() || null; +const webRoleIdsRaw = getArg('webRoleIds'); + +const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +if (!projectRoot || !endpointName || !displayName || !description || !webRoleIdsRaw) { + console.error('Usage: node create-serverlogic-metadata.js --projectRoot --name --displayName --description --webRoleIds '); + process.exit(1); +} + +// Validate endpointName is a safe slug (no path separators or traversal) +if (!/^[a-zA-Z0-9_-]+$/.test(endpointName)) { + console.error(`Error: --name must be a safe slug (alphanumeric, hyphens, underscores only). Got: "${endpointName}"`); + process.exit(1); +} + +const webRoleIds = webRoleIdsRaw.split(',').map(id => id.trim()).filter(Boolean); +if (webRoleIds.length === 0) { + console.error('Error: --webRoleIds must contain at least one UUID'); + process.exit(1); +} + +for (const roleId of webRoleIds) { + if (!UUID_REGEX.test(roleId)) { + console.error(`Error: Invalid UUID in --webRoleIds: "${roleId}"`); + process.exit(1); + } +} + +const serverLogicDir = path.join(projectRoot, '.powerpages-site', 'server-logic', endpointName); +if (!fs.existsSync(serverLogicDir)) { + console.error(`Error: Server logic directory not found at ${serverLogicDir}`); + console.error('Create the server logic folder and JavaScript file before generating metadata.'); + process.exit(1); +} + +const serverLogicScriptPath = path.join(serverLogicDir, `${endpointName}.js`); +if (!fs.existsSync(serverLogicScriptPath)) { + console.error(`Error: Server logic JavaScript file not found at ${serverLogicScriptPath}`); + console.error('Create the server logic JavaScript file before generating metadata.'); + process.exit(1); +} + +const filePath = path.join(serverLogicDir, `${endpointName}.serverlogic.yml`); +if (fs.existsSync(filePath)) { + console.error(`Error: Server logic metadata file already exists at ${filePath}`); + process.exit(1); +} + +const uuid = generateUuid(); + +// Serialize a string value safely for YAML: always single-quote, escaping internal single quotes. +// Rejects newlines since single-quoted YAML scalars cannot span lines without breaking structure. +function yamlStr(val) { + if (/[\r\n]/.test(val)) { + console.error(`Error: Value contains newline characters which are not supported in single-line YAML fields: "${val.slice(0, 50)}..."`); + process.exit(1); + } + return "'" + val.replace(/'/g, "''") + "'"; +} + +const yamlContent = [ + 'adx_serverlogic_adx_webrole:', + ...webRoleIds.map(id => ` - ${id}`), + `description: ${yamlStr(description)}`, + `display_name: ${yamlStr(displayName)}`, + `id: ${uuid}`, + `name: ${endpointName}`, + '', +].join('\n'); + +fs.writeFileSync(filePath, yamlContent, 'utf8'); +process.stdout.write(JSON.stringify({ id: uuid, filePath })); diff --git a/plugins/power-pages/skills/add-server-logic/scripts/validate-serverlogic.js b/plugins/power-pages/skills/add-server-logic/scripts/validate-serverlogic.js new file mode 100644 index 000000000..f68e00dc6 --- /dev/null +++ b/plugins/power-pages/skills/add-server-logic/scripts/validate-serverlogic.js @@ -0,0 +1,314 @@ +#!/usr/bin/env node + +// Validates that Server Logic files were created correctly for a Power Pages code site. +// Checks both the .js code file and the .serverlogic.yml metadata file. +// Runs via the centralized PostToolUse hook to verify the skill produced valid output. + +const fs = require('fs'); +const path = require('path'); +const { approve, block, runValidation, findProjectRoot, UUID_REGEX } = require('../../../scripts/lib/validation-helpers'); + +const ALLOWED_FUNCTIONS = ['get', 'post', 'put', 'patch', 'del']; +const BROWSER_APIS = ['XMLHttpRequest', 'document\\.', 'window\\.', 'setTimeout', 'setInterval', 'navigator\\.', 'fetch']; + +runValidation((cwd) => { + const projectRoot = findProjectRoot(cwd); + if (!projectRoot) return approve(); // Not a Power Pages project, skip + + // Server logic files live inside .powerpages-site/server-logic/ + const serverLogicDir = path.join(projectRoot, '.powerpages-site', 'server-logic'); + if (!fs.existsSync(serverLogicDir)) return approve(); // No server-logic folder, not a server logic session + + const logicDirs = findServerLogicDirs(serverLogicDir); + if (logicDirs.length === 0) return approve(); // No server logic subdirectories, skip + + const errors = []; + + for (const logicDir of logicDirs) { + const dirName = path.basename(logicDir); + const jsFile = path.join(logicDir, `${dirName}.js`); + const ymlFile = path.join(logicDir, `${dirName}.serverlogic.yml`); + + // Validate .js file exists + if (!fs.existsSync(jsFile)) { + errors.push(`${dirName}: missing .js file (expected ${dirName}.js)`); + continue; + } + + // Validate .serverlogic.yml exists + if (!fs.existsSync(ymlFile)) { + errors.push(`${dirName}: missing metadata file (expected ${dirName}.serverlogic.yml)`); + } else { + // Validate YAML contents + const ymlContent = fs.readFileSync(ymlFile, 'utf8'); + + // Check id field exists and is a valid UUID (strip surrounding quotes if present) + const idMatch = ymlContent.match(/^id:\s*(.+)$/m); + if (!idMatch) { + errors.push(`${dirName}.serverlogic.yml: missing 'id' field — PAC CLI requires a GUID`); + } else { + const idValue = idMatch[1].trim().replace(/^['"]|['"]$/g, ''); + if (!UUID_REGEX.test(idValue)) { + errors.push(`${dirName}.serverlogic.yml: 'id' is not a valid UUID: ${idValue}`); + } + } + + // Check adx_serverlogic_adx_webrole is present and non-empty, and validate GUIDs + const webRoleHeaderMatch = /^adx_serverlogic_adx_webrole:\s*$/m.exec(ymlContent); + if (!webRoleHeaderMatch) { + errors.push(`${dirName}.serverlogic.yml: missing 'adx_serverlogic_adx_webrole' field — at least one web role is required`); + } else { + const sectionStart = webRoleHeaderMatch.index + webRoleHeaderMatch[0].length; + const rest = ymlContent.slice(sectionStart); + const nextKeyMatch = rest.match(/^[A-Za-z0-9_]+:\s*/m); + const sectionEnd = nextKeyMatch ? sectionStart + nextKeyMatch.index : ymlContent.length; + const webRoleSection = ymlContent.slice(sectionStart, sectionEnd); + + const roleItemRegex = /^\s*-\s+([^\s#]+)/gm; + let match; + let hasItems = false; + + while ((match = roleItemRegex.exec(webRoleSection)) !== null) { + hasItems = true; + const roleValue = match[1].trim().replace(/^['"]|['"]$/g, ''); + if (!UUID_REGEX.test(roleValue)) { + errors.push(`${dirName}.serverlogic.yml: web role value '${roleValue}' under 'adx_serverlogic_adx_webrole' is not a valid UUID`); + } + } + + if (!hasItems) { + errors.push(`${dirName}.serverlogic.yml: 'adx_serverlogic_adx_webrole' array is empty — at least one web role GUID is required`); + } + } + + // Check name field exists and matches directory name (strip surrounding quotes if present) + const nameMatch = ymlContent.match(/^name:\s*(.+)$/m); + if (!nameMatch) { + errors.push(`${dirName}.serverlogic.yml: missing 'name' field — it must be present and match the folder name '${dirName}'`); + } else { + const nameValue = nameMatch[1].trim().replace(/^['"]|['"]$/g, ''); + if (nameValue !== dirName) { + errors.push(`${dirName}.serverlogic.yml: 'name' field '${nameValue}' does not match folder name '${dirName}'`); + } + } + } + + // Validate .js file contents + const content = fs.readFileSync(jsFile, 'utf8'); + + // Check: file has at least one allowed top-level function (anchored to start of line) + const foundFunctions = ALLOWED_FUNCTIONS.filter(fn => { + const regex = new RegExp(`^(?:async\\s+)?function\\s+${fn}\\s*\\(`, 'm'); + return regex.test(content); + }); + + if (foundFunctions.length === 0) { + errors.push(`${dirName}.js: no allowed top-level functions found (expected: get, post, put, patch, or del)`); + continue; + } + + // Check: CommonJS exports are not allowed (runtime forbids module.exports/exports usage) + if (/(?:^|\n)\s*(?:module\.exports|exports)\.[a-zA-Z0-9_]+\s*=/m.test(content)) { + errors.push(`${dirName}.js: module.exports/exports assignments are not allowed; define top-level get/post/put/patch/del functions instead`); + continue; + } + + // Check: no disallowed top-level functions outside the allowlist (uses brace-depth scan to ignore nested functions) + const allTopLevel = findTopLevelFunctions(content); + const disallowedFunctions = new Set(allTopLevel.filter(name => !ALLOWED_FUNCTIONS.includes(name))); + if (disallowedFunctions.size > 0) { + errors.push(`${dirName}.js: only get, post, put, patch, and del functions are allowed; found additional top-level functions: ${Array.from(disallowedFunctions).join(', ')}`); + continue; + } + + // Check: each function has try/catch (scan until next top-level function or end of file) + // Check: async functions must contain await (unnecessary async causes runtime errors with synchronous Dataverse calls) + for (const fn of foundFunctions) { + const fnRegex = new RegExp(`(async\\s+)?function\\s+${fn}\\s*\\([^)]*\\)\\s*\\{`, 'g'); + const match = fnRegex.exec(content); + if (match) { + const isAsync = !!match[1]; + const bodyStart = match.index + match[0].length; + const nextFnMatch = content.slice(bodyStart).match(/\n(?:async\s+)?function\s+[a-zA-Z]/); + const bodyEnd = nextFnMatch ? bodyStart + nextFnMatch.index : content.length; + const fnBody = content.slice(bodyStart, bodyEnd); + if (!/\btry\s*\{/.test(fnBody)) { + errors.push(`${dirName}.js: function '${fn}' is missing try/catch error handling`); + } else if (!/\bcatch\s*[({]/.test(fnBody)) { + errors.push(`${dirName}.js: function '${fn}' has try but is missing a catch block`); + } + if (isAsync && !/\bawait\b/.test(fnBody)) { + errors.push(`${dirName}.js: function '${fn}' is marked async but contains no await — remove async to avoid runtime errors (Dataverse calls are synchronous, only HttpClient requires async/await)`); + } + } + } + + // Check: each function returns a string-compatible value and uses Server.Logger + for (const fn of foundFunctions) { + const fnRegex = new RegExp(`(?:async\\s+)?function\\s+${fn}\\s*\\([^)]*\\)\\s*\\{`, 'g'); + const match = fnRegex.exec(content); + if (match) { + const bodyStart = match.index + match[0].length; + const nextFnMatch = content.slice(bodyStart).match(/\n(?:async\s+)?function\s+[a-zA-Z]/); + const bodyEnd = nextFnMatch ? bodyStart + nextFnMatch.index : content.length; + const fnBody = content.slice(bodyStart, bodyEnd); + if (!/\breturn\b/.test(fnBody)) { + errors.push(`${dirName}.js: function '${fn}' has no return statement — every function must return a string`); + } else { + // Verify at least one return is string-compatible (string literal, JSON.stringify, String(), or template literal) + const returnRegex = /\breturn\s+([^;]+)/g; + let returnMatch; + let hasStringReturn = false; + while ((returnMatch = returnRegex.exec(fnBody)) !== null) { + const expr = (returnMatch[1] || '').trim(); + if (/^['"`]/.test(expr) || /^JSON\.stringify\s*\(/.test(expr) || /^String\s*\(/.test(expr)) { + hasStringReturn = true; + break; + } + } + if (!hasStringReturn) { + errors.push(`${dirName}.js: function '${fn}' must return a string (use a string literal, JSON.stringify(...), or String(...))`); + } + } + if (!/Server\.Logger/.test(fnBody)) { + errors.push(`${dirName}.js: function '${fn}' is missing Server.Logger calls — every function should log for diagnostics`); + } + } + } + + // Strip comments and string literals so disallowed-token checks don't false-positive + // on occurrences inside documentation comments or string values. + const strippedContent = stripCommentsAndStrings(content); + + // Check: no require/import statements + if (/\brequire\s*\(/.test(strippedContent) || /\bimport\s+/.test(strippedContent)) { + errors.push(`${dirName}.js: contains require() or import — no external dependencies allowed`); + } + + // Check: no browser APIs + for (const api of BROWSER_APIS) { + const regex = new RegExp(`\\b${api}`, 'g'); + if (regex.test(strippedContent)) { + errors.push(`${dirName}.js: contains browser API '${api.replace('\\.', '')}' — not available in server logic runtime`); + } + } + + // Check: no console usage + if (/\bconsole\s*\./.test(strippedContent)) { + errors.push(`${dirName}.js: contains console.* — use Server.Logger instead`); + } + + // Check: no 'function delete()' + 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`); + } + } + + if (errors.length > 0) { + block('Server Logic validation failed:\n- ' + errors.join('\n- ')); + } + + approve(); +}); + +function findServerLogicDirs(dir) { + const dirs = []; + try { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.isDirectory() && !entry.name.startsWith('.')) { + dirs.push(path.join(dir, entry.name)); + } + } + } catch (err) { + throw new Error(`Failed to read server logic directory '${dir}': ${err.message}`); + } + return dirs; +} + +/** + * Replace all comments and string literals with whitespace so that regex + * checks for disallowed tokens don't match inside non-code contexts. + */ +function stripCommentsAndStrings(src) { + let result = ''; + let i = 0; + while (i < src.length) { + const ch = src[i]; + // Line comment + if (ch === '/' && src[i + 1] === '/') { + while (i < src.length && src[i] !== '\n') { result += ' '; i++; } + continue; + } + // Block comment + if (ch === '/' && src[i + 1] === '*') { + result += ' '; i++; + result += ' '; i++; + while (i < src.length - 1 && !(src[i] === '*' && src[i + 1] === '/')) { result += ' '; i++; } + if (i < src.length) { result += ' '; i++; } + if (i < src.length) { result += ' '; i++; } + continue; + } + // String literal + if (ch === '\'' || ch === '"' || ch === '`') { + result += ' '; i++; + while (i < src.length && src[i] !== ch) { + if (src[i] === '\\') { result += ' '; i++; } + result += ' '; i++; + } + if (i < src.length) { result += ' '; i++; } + continue; + } + result += ch; + i++; + } + return result; +} + +/** + * Find all top-level function names using brace-depth tracking. + * Skips string literals and comments so nested functions are not reported. + */ +function findTopLevelFunctions(content) { + const names = []; + let depth = 0; + let i = 0; + while (i < content.length) { + const ch = content[i]; + // Skip line comments + if (ch === '/' && content[i + 1] === '/') { + while (i < content.length && content[i] !== '\n') i++; + continue; + } + // Skip block comments + if (ch === '/' && content[i + 1] === '*') { + i += 2; + while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/')) i++; + i += 2; + continue; + } + // Skip string literals + if (ch === '\'' || ch === '"' || ch === '`') { + i++; + while (i < content.length && content[i] !== ch) { + if (content[i] === '\\') i++; + i++; + } + i++; + continue; + } + if (ch === '{') { depth++; i++; continue; } + if (ch === '}') { depth--; i++; continue; } + // At depth 0, check for function declarations + if (depth === 0) { + const rest = content.slice(i); + const m = rest.match(/^(?:async\s+)?function\s+([a-zA-Z0-9_]+)\s*\(/); + if (m) { + names.push(m[1]); + i += m[0].length; + continue; + } + } + i++; + } + return names; +} diff --git a/plugins/power-pages/skills/audit-permissions/SKILL.md b/plugins/power-pages/skills/audit-permissions/SKILL.md index ebe995f84..411c17e1c 100644 --- a/plugins/power-pages/skills/audit-permissions/SKILL.md +++ b/plugins/power-pages/skills/audit-permissions/SKILL.md @@ -14,6 +14,8 @@ allowed-tools: Read, Write, Bash, Glob, Grep, AskUserQuestion, TaskCreate, TaskU model: opus --- +> **Plugin check**: Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding. + # Audit Permissions Audit existing table permissions on a Power Pages code site. Analyze permissions against the site code and Dataverse metadata, then generate a visual HTML audit report with findings, reasoning, and suggested fixes. @@ -470,11 +472,13 @@ Run the render script (it creates the output directory if needed): node "${CLAUDE_PLUGIN_ROOT}/scripts/render-audit-report.js" --output "" --data "" ``` +The render script refuses to overwrite existing files. Before calling it, check if the default output path (`/docs/permissions-audit.html`) already exists. If it does, choose a new descriptive filename based on context — e.g., `permissions-audit-apr-2026.html`, `permissions-audit-post-migration.html`. Pass the chosen name via `--output`. + Delete the temporary data JSON file after the script succeeds. ### 5.4 Open in Browser -Open the generated HTML file in the user's default browser. +Open the actual output path in the user's default browser. --- diff --git a/plugins/power-pages/skills/create-site/SKILL.md b/plugins/power-pages/skills/create-site/SKILL.md index c1f64ddd1..04ed627e5 100644 --- a/plugins/power-pages/skills/create-site/SKILL.md +++ b/plugins/power-pages/skills/create-site/SKILL.md @@ -7,6 +7,8 @@ allowed-tools: Read, Write, Edit, Grep, Glob, Bash, WebSearch, AskUserQuestion, model: opus --- +> **Plugin check**: Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding. + # Create Power Pages Code Site Guide the user through creating a complete, production-quality Power Pages code site from initial concept to deployed site. Follow a systematic approach: discover requirements, scaffold and launch immediately, plan components and design, implement with design applied, validate, review, and deploy. @@ -279,7 +281,7 @@ The scaffold is a temporary loading screen — it must be **completely replaced* 1. **Design foundations** — **Completely rewrite** `theme.css` (or `styles.css` for Angular) from scratch with the chosen color palette as CSS custom properties, Google Fonts, motion/animation utilities, and background treatments. The scaffold's loading screen CSS is discarded entirely. Commit after this step. 2. **Layout** — **Rewrite** the Layout component (and Header/Footer for Astro) with proper navigation, header, and footer that reflect the chosen design. The scaffold's passthrough Layout is replaced with a real layout structure. 3. **Shared components** — Build reusable components (Navbar, Footer, ContactForm, etc.) that pages will use -4. **Pages** — Create route components for each requested page, **replacing** the scaffold Home page and About placeholder entirely +4. **Pages** — Create route components for each requested page, **replacing** the scaffold Home page and About placeholder entirely. Each page component must update `document.title` on mount to reflect the current page (e.g., `"Contact — Contoso Portal"`). Use the framework's idiomatic lifecycle hook: `useEffect` (React), `onMounted` (Vue), `ngOnInit` (Angular), or a `` tag in the frontmatter (Astro). Format: `"<Page Name> — <Site Name>"`, with the home page using just `"<Site Name>"`. 5. **Router** — Register all new routes (the scaffold only has `/` and `/about` — add all requested routes) 6. **Navigation** — Add links to the new Layout/Header component 7. **Entry HTML** — Update `index.html` (or `Layout.astro` for Astro) to load the chosen Google Fonts instead of the scaffold's DM Sans + Outfit diff --git a/plugins/power-pages/skills/create-webroles/SKILL.md b/plugins/power-pages/skills/create-webroles/SKILL.md index 0336eeff1..9f706ef8d 100644 --- a/plugins/power-pages/skills/create-webroles/SKILL.md +++ b/plugins/power-pages/skills/create-webroles/SKILL.md @@ -10,6 +10,8 @@ allowed-tools: Read, Write, Bash, Grep, Glob, AskUserQuestion, Task, TaskCreate, model: opus --- +> **Plugin check**: Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding. + # Create Web Roles Create web roles for a Power Pages code site. Web roles define the permissions and access levels for different types of site users. diff --git a/plugins/power-pages/skills/deploy-site/SKILL.md b/plugins/power-pages/skills/deploy-site/SKILL.md index 5bcb895a0..b8ffe3a2a 100644 --- a/plugins/power-pages/skills/deploy-site/SKILL.md +++ b/plugins/power-pages/skills/deploy-site/SKILL.md @@ -6,6 +6,8 @@ allowed-tools: Read, Bash, AskUserQuestion, Glob, Grep, TaskCreate, TaskUpdate, model: sonnet --- +> **Plugin check**: Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding. + # Deploy Power Pages Code Site Guide the user through deploying an existing Power Pages code site to a Power Pages environment using PAC CLI. Follow a systematic approach: verify tooling, authenticate, confirm the target environment, build and upload the site, and handle any blockers. diff --git a/plugins/power-pages/skills/integrate-backend/SKILL.md b/plugins/power-pages/skills/integrate-backend/SKILL.md new file mode 100644 index 000000000..6953bf3ed --- /dev/null +++ b/plugins/power-pages/skills/integrate-backend/SKILL.md @@ -0,0 +1,515 @@ +--- +name: integrate-backend +description: > + Use this skill when the user asks to "add backend integration", "connect to data", + "set up backend", "integrate backend", "how should I access data", "add data access", + "add an API", "server-side processing", "cloud flow or web api or server logic", + "which backend approach", "integrate with an external service", "add business logic", + "backend for my site", or wants help deciding between Web API, Server Logic, and + Cloud Flows for their Power Pages site. This skill analyzes the user's business + problem, identifies the right backend integration approach (or combination), and + routes to the appropriate skill. Use this instead of jumping directly to a specific + backend skill when the user's request doesn't clearly map to one approach. +user-invocable: true +argument-hint: describe what your backend needs to do +allowed-tools: Read, Write, Edit, Bash, Grep, Glob, AskUserQuestion, Skill, Task, TaskCreate, TaskUpdate, TaskList +model: opus +--- + +> **Plugin check**: Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding. + +# Backend Integration + +Analyze the user's business problem and recommend the right backend integration approach — **Web API**, **Server Logic**, **Cloud Flows**, or a combination — then route to the appropriate skill(s) to implement the solution. + +## Core Principles + +- **Understand the problem first**: Never jump to a technology choice. Analyze the user's intent, data flow, security needs, and performance requirements before recommending. +- **Recommend the simplest approach that works**: Web API for straightforward Dataverse CRUD, Server Logic when server-side processing is needed, Cloud Flows for async background work. Don't over-engineer. +- **Secure actions belong on the server**: When a write depends on a business rule that must be tamper-proof (state transitions, approval workflows, computed values), the server logic must validate AND execute the write — not just validate and leave the write to a client-side Web API call. See the **Secure Action Principle** in the decision framework. +- **Combinations are normal**: Many real scenarios need more than one approach. Recommend combinations when justified, but explain why each piece is needed. +- **Route, don't implement**: This skill recommends and invokes the right skill(s). It does not create backend files itself. + +**Initial request:** $ARGUMENTS + +--- + +## Workflow + +1. **Verify Site Exists** — Locate the Power Pages project and check prerequisites +2. **Understand the Business Problem** — Analyze what the user needs and why +3. **Recommend Integration Approach** — Present the recommendation with reasoning +4. **Route to Skill(s)** — Invoke the appropriate backend skill(s) to implement + +--- + +## Phase 1: Verify Site Exists + +**Goal**: Locate the Power Pages project root and confirm prerequisites + +**Actions**: + +1. Create todo list with all 4 phases (see [Progress Tracking](#progress-tracking) table) + +### 1.1 Locate Project + +Look for `powerpages.config.json` in the current directory or immediate subdirectories. + +**If not found**: Tell the user to create a site first with `/create-site`. + +### 1.2 Explore Current State + +Use the **Explore agent** to quickly scan the site for existing backend integrations: + +> "Analyze this Power Pages code site for existing backend integrations: +> 1. Check `.powerpages-site/server-logic/` — list any existing server logic endpoints +> 2. Check `.powerpages-site/cloud-flow-consumer/` — list any registered cloud flows +> 3. Search frontend code (`src/**/*.{ts,tsx,js,jsx,vue,astro}`) for calls to `/_api/` (Web API) and `/_api/serverlogics/` (Server Logic) and `/_api/cloudflow/` (Cloud Flows) +> 4. Check for existing service layers or API utilities in `src/services/`, `src/shared/`, or similar +> 5. List available web roles from `.powerpages-site/web-roles/*.webrole.yml` +> Report what backend integrations already exist so we can build on them." + +### 1.3 Discover Dataverse Custom Actions + +Check whether the user's Dataverse environment has existing custom actions that could be leveraged in the integration: + +```powershell +node "${CLAUDE_PLUGIN_ROOT}/scripts/list-custom-actions.js" "<ENV_URL>" +``` + +The script returns Custom APIs (modern) and Custom Process Actions (legacy) with their names, descriptions, binding types, and parameters. If custom actions are found, note them — they will be factored into the recommendation in Phase 3. + +**Output**: Project root confirmed, existing backend integrations identified, Dataverse custom actions discovered + +--- + +## Phase 2: Understand the Business Problem + +**Goal**: Analyze the user's request to understand the underlying business problem, not just the technical ask + +**Actions**: + +### 2.1 Analyze the Request + +From the user's request and the existing site state, determine: + +- **What is the user trying to accomplish?** (business outcome, not technology) +- **What data is involved?** (Dataverse tables, external systems, user input) +- **Who triggers the operation?** (user action, form submit, page load, scheduled) +- **Does the user need an immediate response?** (real-time UI update vs. background processing) +- **Are external services involved?** (payment gateways, email, Graph, SharePoint, third-party APIs) +- **Are credentials or secrets involved?** (API keys, client secrets, tokens) +- **Must logic be hidden from the browser?** (pricing rules, validation algorithms, business rules) +- **Is this a simple data operation or complex business logic?** (CRUD vs. multi-step processing) +- **Does any write depend on a business rule that must be tamper-proof?** (state transitions, approval conditions, computed values) — if yes, the server logic must validate AND execute the write, not just validate +- **Can existing Dataverse custom actions handle part of the requirement?** If custom actions were discovered in Phase 1.3, check whether any align with the user's needs — server logic can wrap existing custom actions via `InvokeCustomApi` instead of building equivalent logic from scratch + +### 2.2 Clarify if Ambiguous + +If the request could map to multiple approaches and the right choice isn't clear, use `AskUserQuestion` to clarify: + +| Question | When to ask | +|----------|-------------| +| Does the user need to see the result immediately, or can it happen in the background? | When the request involves processing that could be sync or async | +| Are external APIs or services involved (e.g., Stripe, SendGrid, SharePoint)? | When the request mentions "integration" without specifics | +| Does this involve sensitive credentials that shouldn't be in the browser? | When external service integration is mentioned | +| Is this a one-time action or a multi-step workflow? | When the request could be a simple call or an orchestration | + +**Output**: Clear understanding of the business problem and technical requirements + +--- + +## Phase 3: Recommend Integration Approach + +**Goal**: Present a recommendation with clear reasoning + +**Actions**: + +### 3.1 Apply the Decision Framework + +> Reference: `${CLAUDE_PLUGIN_ROOT}/skills/integrate-backend/references/decision-framework.md` + +Use the decision matrix, intent mapping, and **Secure Action Principle** from the reference to determine the right approach. Consider: + +1. **Can Web API alone handle this?** If it's straightforward Dataverse CRUD with no external calls, no secrets, no server-side logic, and **no business rules governing the write** — recommend Web API. It's the simplest option. + +2. **Does it need Server Logic?** If any of these apply, Server Logic is needed: + - External API calls (HttpClient) + - Credentials/secrets must stay on the server + - Business logic must be hidden from the browser + - Multiple Dataverse queries should be batched into one endpoint + - Server-side validation that can't be bypassed + - Wrapping a Dataverse Custom API/Action for portal consumption — if custom actions were found in Phase 1.3, check whether any match the requirement before recommending building from scratch + - **The write depends on a business rule that must be tamper-proof** (state transitions, approval conditions, computed values) — server logic must validate AND execute the write + +3. **Does it need Cloud Flows?** If any of these apply, Cloud Flows are the right fit: + - The operation is async — the user doesn't need an immediate result + - Background processing: sending emails, notifications, processing orders + - Multi-step workflows across systems with Power Automate connectors + - Long-running processes that exceed the 120-second server logic timeout + - Non-developers should be able to modify the workflow + +4. **Does it need a combination?** Common combinations: + - Web API + Cloud Flow: UI reads/writes non-sensitive Dataverse fields, some actions trigger background flows + - Server Logic + Cloud Flow: Real-time endpoint validates and executes the action, async flow does follow-up (e.g., server logic transitions status, Cloud Flow sends notification) + - Web API + Server Logic: Web API for safe direct reads/writes, server logic for operations that need business rule enforcement (server logic validates AND writes for those operations) + +### 3.1.1 Security Review — Apply the Secure Action Principle + +Before finalizing the plan, review every item assigned to Web API and ask: **"If a user skipped any preceding server logic validation and called this Web API endpoint directly, could they violate a business rule?"** + +If the answer is **yes**, that write does not belong in a Web API item. Move the write into the server logic item that validates it. The server logic should validate AND execute the write using `Server.Connector.Dataverse`. + +Common patterns that **must** use validate-and-execute server logic (not Web API): + +| Pattern | Why it must be server-side | +|---------|---------------------------| +| Status/state transitions (Draft → Submitted → Approved) | Client could jump to any status by sending a direct PATCH | +| Conditional writes (only allowed before a deadline, only for certain roles) | Client could write after deadline or from wrong role context | +| Computed field writes (server calculates a score, price, or derived value) | Client could submit any value if it writes the field directly | +| Multi-table atomic operations (award bid + reject others + update event) | Partial execution from client could leave data inconsistent | +| Writes that depend on the current state of other records | Client's stale view of data could lead to invalid writes | + +**Correct plan structure for state transitions:** + +``` +Phase 1: Server Logic — "transition-order" endpoint + - POST: accepts { entityId, targetStatus } + - Reads current record, validates transition is allowed, writes new status + - Returns { success, previousStatus, newStatus } + +Phase 2: Web API — Order table CRUD + - Read: list/filter orders (safe for Web API) + - Create: new orders in Draft status (safe — initial state, no rule to enforce) + - Update: description, notes, dates (safe — no business rules on these fields) + - NOTE: Status changes are NOT here — they go through the server logic endpoint +``` + +**Incorrect plan structure (anti-pattern):** + +``` +❌ Phase 1: Server Logic — "validate-transition" endpoint + - POST: accepts { entityId, targetStatus } + - Reads current record, validates transition + - Returns { valid: true/false } ← only validates, doesn't write + +❌ Phase 2: Web API — Order table CRUD + - Update: includes status field ← client writes status after "validation" + - PROBLEM: client can skip Phase 1 and write any status directly +``` + +### 3.2 Render the HTML Plan + +Build the plan data and render an HTML plan before asking for approval. The plan visualizes: + +- **Key Concepts** — Educational overview of Web API, Server Logic, and Cloud Flows (hardcoded in template) +- **Overview** — Stats per approach, approach chips, design rationale +- **Data Flow** — Visual flow diagrams showing how data moves for each user action, with steps color-coded by approach +- **Implementation Order** — Phase-grouped items with dependencies, complexity badges, and implementation commands +- **Integration Items** — Each item with its approach, reasoning, and implementation details + +Prepare a JSON object with these keys: + +| Key | Description | +|-----|-------------| +| `SITE_NAME` | Site name from `powerpages.config.json` | +| `PLAN_TITLE` | Short title (e.g., "Backend Integration Plan") | +| `SUMMARY` | 1-3 sentence summary of the integration strategy | +| `ITEMS_DATA` | Array of integration items (see format below) | +| `DATA_FLOWS_DATA` | Array of data flow diagrams (see format below) | +| `RATIONALE_DATA` | Array of design rationale entries (`icon`, `title`, `desc`) | + +**ITEMS_DATA format:** +```json +{ + "name": "Create PayPal Order", + "approach": "webapi|serverlogic|cloudflow", + "description": "What this item does", + "reasoning": "Why this approach was chosen", + "phase": 1, + "status": "new|existing|extends", + "complexity": "low|medium|high", + "depends": "Name of item this depends on (if any)", + "details": [ + { "label": "Endpoint", "value": "/_api/serverlogics/create-paypal-order" }, + { "label": "Secrets", "value": "PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET" } + ], + "docs": [ + { "label": "Server Logic Overview", "url": "https://learn.microsoft.com/..." } + ] +} +``` + +**Phase assignment rules** — assign a `phase` number to each item based on dependencies: + +1. Items with no dependencies go in the earliest phase appropriate for their approach +2. Items that depend on other items go in a later phase than their dependency +3. Items in the **same phase have no dependencies on each other** and can be built in parallel +4. Recommended default ordering: Server Logic foundations first (validate-and-execute endpoints for state transitions, batch queries), then Web API CRUD for non-sensitive fields (reads, creates with safe defaults, updates to fields with no business rules), then advanced Server Logic (multi-table transactions), then Cloud Flows (async follow-ups) +5. **Security constraint**: A Web API item must never write a field whose value is governed by a business rule enforced in a server logic item. If a field needs validation, the server logic item should write it directly — the Web API item should exclude that field from its scope + +**DATA_FLOWS_DATA format:** +```json +{ + "trigger": "User registers and pays", + "description": "User fills the form, pays via PayPal, receives confirmation", + "steps": [ + { "approach": "serverlogic", "name": "Validate Seats", "detail": "Check availability" }, + { "approach": "serverlogic", "name": "Create Order", "detail": "Server calls PayPal" }, + { "approach": "cloudflow", "name": "Send Email", "detail": "Async confirmation" } + ] +} +``` + +**Important**: In data flow diagrams, when a server logic step validates a business rule, the next step should NOT be a Web API write for the same field. The server logic step should validate AND execute. For example: + +```json +// ✅ Correct — server logic validates and writes status +{ "approach": "serverlogic", "name": "Submit Order", "detail": "Validates Draft→Submitted, writes new status" } + +// ❌ Incorrect — split across server logic validation and Web API write +{ "approach": "serverlogic", "name": "Validate Transition", "detail": "Checks Draft→Submitted" }, +{ "approach": "webapi", "name": "Update Status", "detail": "PATCH status to Submitted" } +``` +``` + +Write the plan to `<PROJECT_ROOT>/docs/backend-plan.html` (create `docs/` if needed). Use the render script: + +```powershell +node "${CLAUDE_PLUGIN_ROOT}/scripts/render-backend-plan.js" --output "<OUTPUT_PATH>" --data "<DATA_JSON_PATH>" +``` + +The render script refuses to overwrite existing files. If the default path exists, choose a new descriptive filename (e.g., `backend-plan-payments.html`). + +After rendering, open the HTML plan in the user's default browser: + +```bash +open "<OUTPUT_PATH>" # macOS +# start "<OUTPUT_PATH>" # Windows +# xdg-open "<OUTPUT_PATH>" # Linux +``` + +### 3.3 Present Plan Summary + +Do **not** restate the full plan in the CLI. The HTML file is the single detailed plan artifact. + +In the CLI, give only a brief summary: +- Total items and how many per approach +- Whether the plan uses one approach or a combination +- The actual output path +- A note that the browser-opened HTML contains the full details including data flow diagrams + +### 3.4 Confirm with User + +Use `AskUserQuestion`: + +| Question | Options | +|----------|---------| +| Here's the integration plan. The HTML plan is open in your browser with data flow diagrams and per-item reasoning. Does this approach look right? | Yes, proceed (Recommended), Change approach, Cancel | + +**If "Change approach"**: Ask what they'd prefer and why, update the plan, and present again. + +**If "Cancel"**: Stop the workflow. + +**Output**: User-approved integration approach + +--- + +## Phase 4: Route to Skill(s) + +**Goal**: Invoke the appropriate skill(s) to implement the approved approach, respecting phase ordering and building in parallel within each phase + +**Actions**: + +### 4.1 Build the Phase Execution Plan + +Group the approved items by their `phase` number from the plan. Each phase is a batch of independent items — items within a phase have no dependencies on each other and can be built in parallel. + +| Approach | Skill to invoke | What to pass | +|----------|----------------|--------------| +| Web API | `/integrate-webapi` | The user's request + tables for this phase + existing patterns | +| Server Logic | `/add-server-logic` | The user's request + endpoints for this phase + SDK features needed + secrets identified + any matching Dataverse custom actions from Phase 1.3 | +| Cloud Flow | `/add-cloud-flow` | The user's request + async operations for this phase | + +### 4.2 Execute Phase by Phase + +Process phases in order (Phase 1, then Phase 2, etc.). **Complete all items in a phase before moving to the next** — later phases depend on earlier phases. + +**Within each phase**, maximize parallelism: + +- **Single approach in the phase**: Invoke the skill once with all items for that phase. Tell the skill: *"These N items are independent — implement them in parallel where possible."* +- **Multiple approaches in the same phase**: Invoke each skill for its items. Since items in the same phase have no cross-dependencies, the order of skill invocation within a phase does not matter. When invoking the second skill, pass context from the first so it can follow the same frontend patterns (e.g., naming conventions, file organization). + +**Example** — a plan with 4 phases: + +| Phase | Items | Skill(s) | Parallelism | +|-------|-------|----------|-------------| +| 1 | Validate Transition (serverlogic), Dashboard Metrics (serverlogic) | `/add-server-logic` | Both items passed together — skill builds them in parallel | +| 2 | Supplier Updates (webapi), Bid CRUD (webapi), PR Creation (webapi) | `/integrate-webapi` | All 3 items passed together — skill builds them in parallel | +| 3 | Award Bid (serverlogic) | `/add-server-logic` | Single item — sequential | +| 4 | Approval Notification (cloudflow), Expiry Alerts (cloudflow) | `/add-cloud-flow` | Both items passed together — skill builds them in parallel | + +When invoking each skill, include: +1. **Which items to implement** — list the specific item names from the plan for this phase +2. **Parallelism guidance** — *"These items are in the same phase and have no dependencies on each other. Implement them in parallel where possible."* +3. **Context from previous phases** — what was created so far (files, patterns, services) so the skill can build on it + +### 4.3 Summary + +After all phases complete, present a brief summary of everything that was created: + +| Phase | Approach | What was created | +|-------|----------|-----------------| +| 1 | Server Logic | [endpoints created, SDK features used] | +| 2 | Web API | [files created, tables integrated] | +| 3 | Server Logic | [endpoints created] | +| 4 | Cloud Flow | [flows registered, triggers wired] | + +Remind the user to deploy with `/deploy-site` if they haven't already. + +### 4.4 Record Skill Usage + +> Reference: `${CLAUDE_PLUGIN_ROOT}/references/skill-tracking-reference.md` + +Follow the skill tracking instructions in the reference to record this skill's usage. Use `--skillName "IntegrateBackend"`. + +**Output**: All recommended backend integrations implemented + +--- + +## Important Notes + +### When NOT to Use This Skill + +If the user's request clearly and unambiguously maps to a single approach, **skip this skill and go directly to the implementation skill**: + +- "Create a server logic endpoint for..." → `/add-server-logic` +- "Integrate Web API for the contacts table" → `/integrate-webapi` +- "Add a cloud flow for sending emails" → `/add-cloud-flow` + +This skill is for **ambiguous requests** where the user describes a business problem and needs help choosing the right approach. + +### Examples: What Routing Looks Like + +**Example 1: Simple Dataverse CRUD → Web API** +``` +User: I need to show a list of products on the homepage and let users + filter by category. + +Recommendation: Web API +Reason: This is straightforward Dataverse read operations with filtering. + No external APIs, no secrets, no server-side logic needed. +Skill: /integrate-webapi +``` + +**Example 2: External API with credentials → Server Logic** +``` +User: Add payment processing through Stripe. The API key must stay + on the server. + +Recommendation: Server Logic +Reason: Calls an external API (Stripe) with credentials that must be + protected. Server logic hides the code and credentials from + the browser. +Skill: /add-server-logic +``` + +**Example 3: Background email → Cloud Flow** +``` +User: When a user submits the contact form, send them a confirmation + email and notify the support team on Teams. + +Recommendation: Cloud Flow +Reason: Email and Teams notifications are async — the user doesn't + need to wait for them. Cloud Flows have built-in connectors + for Outlook and Teams. +Skill: /add-cloud-flow +``` + +**Example 4: Server-side validation → Server Logic (validate-and-execute)** +``` +User: Add validation that rejects orders when quantity exceeds + inventory. Check the actual Dataverse data, not just the form. + +Recommendation: Server Logic (validate-and-execute) +Reason: Server-side validation that can't be bypassed from the browser. + The server logic checks inventory AND creates/updates the order + in a single call — the client doesn't write the order via Web API + because quantity validation would be bypassable. +Skill: /add-server-logic +``` + +**Example 5: Dashboard performance → Server Logic** +``` +User: The dashboard makes 3 separate API calls to load contacts, + orders, and products. It's slow. + +Recommendation: Server Logic +Reason: Batching multiple Dataverse queries into a single server + endpoint reduces round-trips and improves load time. +Skill: /add-server-logic +``` + +**Example 6: CRUD + background processing → Web API + Cloud Flow** +``` +User: Let users submit support tickets from the portal. After + submission, assign it to the right team and send an email. + +Recommendation: Web API + Cloud Flow +Reason: The ticket creation is a Dataverse write (Web API). The + assignment and email happen in the background after the + user submits (Cloud Flow). +Phases: Phase 1 → /integrate-webapi (ticket CRUD) + Phase 2 → /add-cloud-flow (assignment + email, depends on ticket creation) +``` + +**Example 7: Validate + process + notify → Server Logic + Cloud Flow** +``` +User: When a user places an order, validate inventory, process the + payment through Stripe, and send a confirmation email. + +Recommendation: Server Logic + Cloud Flow +Reason: Inventory validation and Stripe payment need real-time + server-side processing with credentials (Server Logic). + The confirmation email is async (Cloud Flow). +Phases: Phase 1 → /add-server-logic (validate inventory + process payment — parallel) + Phase 2 → /add-cloud-flow (confirmation email, depends on payment) +``` + +**Example 8: State transitions + CRUD → Server Logic (validate-and-execute) + Web API** +``` +User: Build a procurement workflow with status transitions + (Draft → Submitted → Approved) and let users edit request details. + +Recommendation: Server Logic + Web API +Reason: Status transitions must be tamper-proof — server logic validates + the transition AND writes the new status to Dataverse in one call + (the Secure Action Principle). Editing non-sensitive fields like + description or notes is safe via Web API since no business rule + governs those writes. +Phases: Phase 1 → /add-server-logic (transition-request endpoint that + validates AND writes status changes) + Phase 2 → /integrate-webapi (read/list requests, edit description + and notes — but NOT status, which goes through server logic) + +NOTE: The Web API item must NOT include the status field in its Update + operations. Status writes go exclusively through the server logic + endpoint. +``` + +### Progress Tracking + +Before starting Phase 1, create a task list with all phases using `TaskCreate`: + +| Task subject | activeForm | Description | +|-------------|------------|-------------| +| Verify site exists | Verifying site prerequisites | Locate project root, scan for existing backend integrations | +| Understand business problem | Analyzing requirements | Determine what the user needs, clarify ambiguities | +| Recommend integration approach | Evaluating approaches | Apply decision framework, present recommendation | +| Route to implementation skill(s) | Implementing backend integration | Invoke the approved skill(s) and summarize results | + +Mark each task `in_progress` when starting and `completed` when done via `TaskUpdate`. + +--- + +**Begin with Phase 1: Verify Site Exists** diff --git a/plugins/power-pages/skills/integrate-backend/assets/backend-plan.html b/plugins/power-pages/skills/integrate-backend/assets/backend-plan.html new file mode 100644 index 000000000..35306403d --- /dev/null +++ b/plugins/power-pages/skills/integrate-backend/assets/backend-plan.html @@ -0,0 +1,563 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="UTF-8"/> +<meta name="viewport" content="width=device-width,initial-scale=1.0"/> +<title>__PLAN_TITLE__ - __SITE_NAME__ + + + + +
+
+
+ +
+
__PLAN_TITLE__
+
__SITE_NAME__
+
+
+
+
+ +
+ + +
+ +
+

Key Concepts

+

Power Pages offers three ways to add backend logic. Understanding when to use each is the key to a well-architected site.

+
+
+ + +
+

Integration Plan

+

Backend integration strategy for __SITE_NAME__

+ +
__SUMMARY__
+ +
+
0
Web API
+
0
Server Logic
+
0
Cloud Flows
+
+ +
+
Approaches Used
+
+
+ +

Design Rationale

+
+
+ + +
+

Data Flow

+

How data moves through the integration — from user action to backend and back

+
+
+ + +
+

Implementation Order

+

Phases are ordered by dependency — complete each phase before moving to the next. Items within a phase can be implemented in parallel.

+
+
+ + +
+

Integration Items

+

+
+
+
+
+
+ + + + diff --git a/plugins/power-pages/skills/integrate-backend/references/decision-framework.md b/plugins/power-pages/skills/integrate-backend/references/decision-framework.md new file mode 100644 index 000000000..29755cf2c --- /dev/null +++ b/plugins/power-pages/skills/integrate-backend/references/decision-framework.md @@ -0,0 +1,238 @@ +# Backend Integration Decision Framework + +Use this framework to recommend the right backend integration approach for a Power Pages code site. A single user request may map to one approach or a combination. +## The Three Approaches + +### Web API (`/integrate-webapi`) + +**What it is:** A client-side, browser-based OData API that lets frontend code perform CRUD operations directly against Dataverse tables via `/_api/` endpoints. + +**How it works:** JavaScript/TypeScript in the browser makes HTTP calls to Dataverse. Authentication is cookie-based (user session). Table permissions and web roles control access. No code runs on the server — the browser does all the work. + +**Best for:** +- Displaying Dataverse records in the UI (lists, tables, dashboards, detail views) +- Form submissions that create or update Dataverse records +- Filtering, sorting, searching records with OData queries +- Inline editing of records +- File/image upload to Dataverse File columns +- Real-time data binding where the user sees results immediately +- Aggregation queries (`$apply`) for charts and summaries + +**Not suitable when:** +- The logic requires calling external APIs (Stripe, SendGrid, Graph, etc.) +- API keys, client secrets, or other credentials are involved +- Business logic must be hidden from the browser (pricing rules, validation algorithms) +- The operation needs to batch multiple table queries into one call for performance +- The write depends on a business rule that must be tamper-proof (e.g., status transitions, approval workflows) — use Server Logic to validate AND execute the write in a single call instead (see Secure Action Principle) +- The operation should happen in the background after the user moves on + +**Key characteristics:** +- Code runs in the browser (visible in DevTools) +- No external API access +- No credential/secret handling +- Real-time, synchronous responses +- Requires table permissions for every Dataverse table accessed +- CSRF token required for mutations (POST, PATCH, DELETE) + +--- + +### Server Logic (`/add-server-logic`) + +**What it is:** Server-side JavaScript that runs in a sandboxed V8 engine on the Power Pages server. Exposed as REST endpoints at `/_api/serverlogics/`. Code is hidden from the browser. + +**How it works:** The frontend calls a server logic endpoint. The server executes the JavaScript function matching the HTTP method (get, post, put, patch, del). The function can access Dataverse, call external APIs, read site settings/environment variables, and return a computed response. + +**Best for (by category):** + +| Category | Use Case | Example | +|----------|----------|---------| +| **Security** | Secure content rendering | Healthcare portal: patient data after server-side role check | +| | Secret & credential management | Stripe API key stays on server; client never sees it | +| | Server-side validation | Reject order if quantity exceeds inventory | +| | Rate limiting / abuse prevention | Max 5 support tickets/hour/user, enforced server-side | +| **Authorization** | Complex permissions beyond table permissions | Moderator edits only in their assigned community | +| | Row-level logic | Manager approves expenses for direct reports < $1K | +| **Data Integrity** | Cross-entity transactions | Order + line items + inventory: all roll back if one fails | +| | Computed data | Insurance premium calculated server-side; client sees result | +| | Business rule enforcement (validate-and-execute) | Permit: Submitted > Review > Approved — server logic validates the transition AND writes the new status to Dataverse in a single call, so the client never writes the status field directly | +| | State machine transitions | Server logic reads current status, validates the target status is reachable, and performs the update — preventing clients from jumping to arbitrary states | +| **Performance** | Batch operations | Dashboard: Contacts + Orders + Products in one call | +| | Data aggregation | 12 monthly totals instead of 10,000 raw rows | +| | Response formatting | JSON, CSV, or XML based on caller request | +| **Integration** | Third-party services | PayPal/Stripe payment via server-side call | +| | On-prem services | ERP via Azure Relay for stock levels | +| | Microsoft Graph / SharePoint | Upload documents, read SharePoint lists via OAuth | +| | Wrapping Dataverse Custom APIs/Actions | Expose existing Dataverse custom actions (both Custom APIs and Custom Process Actions) to the portal via `InvokeCustomApi`. Discover available actions with `list-custom-actions.js` before recommending building from scratch — the customer may already have actions that do what's needed. | + +**Not suitable when:** +- The operation is purely async/background (no immediate response needed) — use Cloud Flows instead +- The scenario only needs simple Dataverse CRUD with no extra logic — Web API is simpler +- The workflow spans multiple systems with built-in connectors (e.g., send email + create record + notify Teams) — Cloud Flows have 400+ connectors +- The operation takes longer than 120 seconds (platform maximum timeout) + +**Key characteristics:** +- Code runs on the server (hidden from browser) +- Can call external APIs via `Server.Connector.HttpClient` +- Can access Dataverse via `Server.Connector.Dataverse` (respects table permissions) +- Can read site settings and environment variables for credential management +- 5 functions only: get, post, put, patch, del — each must return a string +- ECMAScript 2023 sandbox, no npm packages, no browser APIs +- 120-second maximum timeout, 10 MB default memory +- CSRF token required for non-GET requests + +--- + +### Cloud Flows (`/add-cloud-flow`) + +**What it is:** Power Automate cloud flows triggered from the Power Pages frontend. The flow runs asynchronously in the Power Automate service and has access to 400+ connectors. + +**How it works:** The frontend calls a registered cloud flow endpoint. The flow runs in the background on Power Automate infrastructure. The user does not wait for the flow to complete — the trigger returns immediately with a confirmation. + +**Best for:** +- Background/async processing where the user doesn't need an immediate result +- Sending emails or notifications after a form submission +- Processing orders, approvals, or multi-step business workflows +- Integrating with systems that have Power Automate connectors (Teams, Outlook, SharePoint, Dynamics 365, SAP, ServiceNow, etc.) +- Long-running processes that exceed the 120-second server logic timeout +- Orchestrating multi-step workflows across multiple systems +- Scenarios where no-code/low-code maintainability is important (business users can modify flows) + +**Not suitable when:** +- The frontend needs an immediate, computed response (use Server Logic) +- The operation is simple Dataverse CRUD (use Web API) +- The logic needs to return data that the UI renders immediately (use Server Logic or Web API) +- Low latency is critical — flow trigger has overhead compared to direct API calls + +**Key characteristics:** +- Runs asynchronously in Power Automate (fire-and-forget from the frontend) +- 400+ pre-built connectors +- No-code/low-code — modifiable by business users in the Power Automate designer +- Output is not immediately consumed by the user +- Registered via `.cloudflowconsumer.yml` metadata files +- Requires web role assignments for authorization + +--- + +## Decision Matrix + +Use these questions to narrow down the recommendation: + +| Question | Web API | Server Logic | Cloud Flow | +|----------|:-------:|:------------:|:----------:| +| Does the UI need to display data from Dataverse? | **Yes** | Possible | No | +| Does it call external APIs (non-Dataverse)? | No | **Yes** | Possible | +| Are credentials/secrets involved? | No | **Yes** | Possible | +| Must business logic be hidden from the browser? | No | **Yes** | N/A | +| Does the write depend on a business rule that must be tamper-proof? | No | **Yes (validate-and-execute)** | No | +| Is the operation async/background (user doesn't wait)? | No | No | **Yes** | +| Is it a simple Dataverse CRUD with no extra logic? | **Yes** | Overkill | Overkill | +| Does it need 400+ connectors (Teams, Outlook, SAP)? | No | No | **Yes** | +| Should the response render immediately in the UI? | **Yes** | **Yes** | No | +| Does it batch multiple queries for performance? | No | **Yes** | No | +| Is it a long-running process (>120 seconds)? | No | No | **Yes** | +| Should non-developers be able to modify the logic? | No | No | **Yes** | + +## The Secure Action Principle + +**When server logic validates a business rule, it must also execute the resulting action.** + +This is the most important architectural principle for secure backend integration. Splitting validation from execution — where server logic validates a rule and then a separate client-side Web API call performs the write — creates a security gap because the client can skip the validation call and write directly via Web API. + +### Anti-Pattern: Validate-Only Server Logic + Client-Side Write + +``` +❌ INSECURE — Do NOT use this pattern for security-sensitive operations: + +1. Browser calls /_api/serverlogics/validate-transition (Server Logic checks Draft → Submitted is valid) +2. Server Logic returns { valid: true } +3. Browser calls /_api/cr65f_orders(id) with PATCH { status: "Submitted" } (Web API writes the change) + +Problem: A user can skip step 1 and go directly to step 3 via browser dev tools, +bypassing all server-side validation. +``` + +### Correct Pattern: Validate-and-Execute Server Logic + +``` +✅ SECURE — Server logic validates AND executes: + +1. Browser calls /_api/serverlogics/transition-order with POST { orderId: "...", newStatus: "Submitted" } +2. Server Logic reads current record from Dataverse, validates Draft → Submitted is allowed +3. Server Logic writes the status change to Dataverse via Server.Connector.Dataverse.UpdateRecord +4. Server Logic returns { status: "success", previousStatus: "Draft", newStatus: "Submitted" } + +The browser never makes a direct Web API write for this operation. +``` + +### When Does This Apply? + +Use validate-and-execute (server logic performs the write) whenever **any** of these are true: + +| Condition | Example | +|-----------|---------| +| The operation enforces a state machine or lifecycle | Order status: Draft → Submitted → Approved → Fulfilled | +| The write depends on a business rule that must be tamper-proof | "Only allow bid submission before the deadline" | +| The operation spans multiple tables atomically | Award a bid + reject all others + update RFx status | +| The write involves computed or derived values the client shouldn't control | Server calculates a discount or score and writes it | +| The operation requires authorization beyond table permissions | "Managers can only approve expenses for their direct reports" | +| The client should not have write access to the field at all | Status fields that follow strict transitions | + +Use Web API for the write (validation-only server logic is fine) when **all** of these are true: + +| Condition | Example | +|-----------|---------| +| The write is simple CRUD with no business rules | Editing a name or description field | +| Table permissions alone enforce the access control | User edits their own profile | +| The fields being written have no restricted value constraints | Free-text fields, dates the user picks | +| Skipping validation would not cause a security or data integrity issue | Updating a contact's phone number | + +### Impact on Plan Design + +When building integration plans, this principle affects how items are assigned to approaches: + +1. **State transitions** — The server logic endpoint should accept the entity ID and target status, validate the transition, and write the new status to Dataverse. The frontend calls only the server logic endpoint — there is no separate Web API PATCH for the status field. + +2. **Multi-step operations** — When an action involves validation + write + side effects (e.g., award a bid, reject losers, update event status), the entire sequence belongs in one server logic endpoint. The frontend makes a single call. + +3. **Mixed operations on the same table** — A table may use Web API for some fields (e.g., editing a description) and server logic for others (e.g., changing status). This is expected and correct. Table permissions should grant read access broadly but restrict write access to fields that are safe for direct client writes. + +4. **Phase ordering** — Server logic endpoints that validate-and-execute should be built before any dependent frontend work. The frontend for these operations calls server logic, not Web API. + +--- + +## Common Combinations + +Many real-world scenarios use multiple approaches together: + +| Combination | When to use | Example | +|-------------|-------------|---------| +| **Web API + Server Logic** | UI reads/writes non-sensitive fields directly, but security-sensitive operations go through server logic that validates and executes | Dashboard displays records via Web API; status transitions go through server logic that validates and writes | +| **Server Logic + Cloud Flow** | Real-time endpoint validates and executes the action, then async processing follows | Server logic validates transition and writes the new status, then a Cloud Flow sends the notification email | +| **Web API + Cloud Flow** | UI manages data directly (no business rules on the write), and some actions trigger background workflows | User edits a description via Web API; a separate "Submit for Approval" action triggers a Cloud Flow | +| **All three** | Complex application with safe direct writes, secure server-side actions, and automation | Web API for browsing/editing non-sensitive fields, Server Logic for state transitions and payment processing, Cloud Flow for notifications | + +## Mapping User Intent to Approach + +| User says... | Likely approach | Reasoning | +|--------------|-----------------|-----------| +| "Show data from Dataverse" / "display records" / "CRUD" | Web API | Direct data access, real-time UI binding | +| "Filter and sort products" / "search contacts" | Web API | Standard OData queries, no server logic needed | +| "Call an external API" / "integrate with Stripe/Twilio/etc." | Server Logic | External API calls with credential protection | +| "Add validation on the server" / "prevent bypassing" | Server Logic | Server-side enforcement (security) | +| "Rate limit submissions" / "prevent abuse" | Server Logic | Server-side enforcement (security) | +| "Calculate premium/price/discount on the server" | Server Logic | Computed data — logic hidden from browser | +| "Enforce a workflow sequence" / "status transitions" | Server Logic (validate-and-execute) | Server logic validates the transition AND writes the new status — the client never writes status directly via Web API | +| "Only let managers approve their team's expenses" | Server Logic | Row-level authorization logic | +| "Connect to our on-prem ERP" / "Azure Relay" | Server Logic | On-prem integration via server-side call | +| "Return data as CSV/XML" / "format the response" | Server Logic | Response formatting (performance) | +| "Send an email when..." / "notify the team when..." | Cloud Flow | Async notification, no immediate UI response | +| "Process orders in the background" | Cloud Flow | Background processing, user doesn't wait | +| "Batch multiple API calls" / "dashboard loads too slow" | Server Logic | Combine multiple queries into one endpoint | +| "Upload to SharePoint" / "call Microsoft Graph" | Server Logic | External API with OAuth credentials | +| "Use an existing custom action" / "call a Dataverse custom API" / "wrap a custom action" | Server Logic | Existing Dataverse custom action exposed to portal via `InvokeCustomApi` — discover available actions first | +| "Add an approval workflow" | Cloud Flow | Multi-step workflow with connectors | +| "Hide pricing logic from the browser" | Server Logic | Code hidden from client | +| "Bulk import CSV" / "process file in background" | Cloud Flow | Long-running background processing | +| "Create a record and send a confirmation email" | Web API + Cloud Flow | CRUD is immediate, email is async | +| "Validate inventory and process payment" | Server Logic (both) | Server-side validation + external API call | +| "Submit form, assign to team, and email confirmation" | Web API + Cloud Flow | Dataverse write + async assignment/email | diff --git a/plugins/power-pages/skills/integrate-webapi/SKILL.md b/plugins/power-pages/skills/integrate-webapi/SKILL.md index 9162103b1..56862f1b5 100644 --- a/plugins/power-pages/skills/integrate-webapi/SKILL.md +++ b/plugins/power-pages/skills/integrate-webapi/SKILL.md @@ -13,6 +13,8 @@ allowed-tools: Read, Write, Edit, Bash, Grep, Glob, AskUserQuestion, Task, TaskC model: opus --- +> **Plugin check**: Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding. + # Integrate Web API Integrate Power Pages Web API into a code site's frontend. This skill orchestrates the full lifecycle: analyzing where integrations are needed, implementing API client code for each table, configuring permissions and site settings, and deploying the site. diff --git a/plugins/power-pages/skills/setup-auth/SKILL.md b/plugins/power-pages/skills/setup-auth/SKILL.md index 4ddab5636..c53e73b48 100644 --- a/plugins/power-pages/skills/setup-auth/SKILL.md +++ b/plugins/power-pages/skills/setup-auth/SKILL.md @@ -13,6 +13,8 @@ allowed-tools: Read, Write, Edit, Bash, Grep, Glob, AskUserQuestion, Task, TaskC model: opus --- +> **Plugin check**: Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding. + # Set Up Authentication & Authorization Configure authentication (login/logout via Microsoft Entra ID) and role-based authorization for a Power Pages code site. This skill creates an auth service, type declarations, authorization utilities, auth UI components, and role-based access control patterns appropriate to the site's framework. diff --git a/plugins/power-pages/skills/setup-datamodel/SKILL.md b/plugins/power-pages/skills/setup-datamodel/SKILL.md index d3168daaa..12d953f43 100644 --- a/plugins/power-pages/skills/setup-datamodel/SKILL.md +++ b/plugins/power-pages/skills/setup-datamodel/SKILL.md @@ -11,6 +11,8 @@ allowed-tools: Read, Write, Bash, Grep, Glob, AskUserQuestion, Task, TaskCreate, model: opus --- +> **Plugin check**: Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding. + # Set Up Dataverse Data Model Guide the user through creating Dataverse tables, columns, and relationships for their Power Pages site. Follow a systematic approach: verify prerequisites, obtain a data model (via AI analysis or user-provided diagram), review and approve, then create all schema objects via OData API. diff --git a/plugins/power-pages/skills/test-site/SKILL.md b/plugins/power-pages/skills/test-site/SKILL.md index 2d87d0559..b500b5b79 100644 --- a/plugins/power-pages/skills/test-site/SKILL.md +++ b/plugins/power-pages/skills/test-site/SKILL.md @@ -12,6 +12,8 @@ allowed-tools: Read, Bash, Glob, Grep, AskUserQuestion, TaskCreate, TaskUpdate, model: opus --- +> **Plugin check**: Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding. + # Test Power Pages Site Test a deployed, activated Power Pages site at runtime. Navigate the site in a browser, crawl all discoverable links, verify pages load correctly, capture network traffic to test API requests, and generate a comprehensive test report. diff --git a/scripts/ensure-skill-version-check.js b/scripts/ensure-skill-version-check.js new file mode 100644 index 000000000..cb1b35a56 --- /dev/null +++ b/scripts/ensure-skill-version-check.js @@ -0,0 +1,76 @@ +#!/usr/bin/env node + +/** + * Ensures every Power Pages SKILL.md has the plugin version check line + * immediately after the YAML frontmatter closing ---. + * + * Usage: + * node scripts/ensure-skill-version-check.js # auto-add missing lines + * node scripts/ensure-skill-version-check.js --check # CI: fail if any are missing + */ + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '..'); +const SKILLS_DIR = path.join(ROOT, 'plugins', 'power-pages', 'skills'); +const VERSION_CHECK_LINE = + '> **Plugin check**: Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding.'; + +const checkOnly = process.argv.includes('--check'); + +function getSkillFiles() { + if (!fs.existsSync(SKILLS_DIR)) return []; + return fs + .readdirSync(SKILLS_DIR, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => path.join(SKILLS_DIR, d.name, 'SKILL.md')) + .filter((f) => fs.existsSync(f)); +} + +function hasVersionCheck(content) { + return content.includes(VERSION_CHECK_LINE); +} + +function addVersionCheck(content) { + // Match YAML frontmatter: starts with --- on its own line, ends with --- on its own line. + // Use line-based matching to avoid false positives from --- inside body or values, + // and handle both LF and CRLF line endings. + const match = content.match(/^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n/); + if (!match) return content; + const insertPos = match[0].length; + return ( + content.slice(0, insertPos) + + '\n' + + VERSION_CHECK_LINE + + '\n' + + content.slice(insertPos) + ); +} + +const skillFiles = getSkillFiles(); +const missing = []; + +for (const filePath of skillFiles) { + const content = fs.readFileSync(filePath, 'utf8'); + if (!hasVersionCheck(content)) { + missing.push(filePath); + if (!checkOnly) { + const updated = addVersionCheck(content); + fs.writeFileSync(filePath, updated, 'utf8'); + console.log(`Added version check: ${path.relative(ROOT, filePath)}`); + } + } +} + +if (missing.length === 0) { + console.log('All SKILL.md files have the plugin version check.'); +} else if (checkOnly) { + console.log('The following SKILL.md files are missing the plugin version check:'); + for (const f of missing) { + console.log(` - ${path.relative(ROOT, f)}`); + } + process.exit(1); +} else { + console.log(`Added version check to ${missing.length} file(s).`); +}