diff --git a/.github/workflows/issue-triage.lock.yml b/.github/workflows/issue-triage.lock.yml
index 3249affe00..3fc2d5d190 100644
--- a/.github/workflows/issue-triage.lock.yml
+++ b/.github/workflows/issue-triage.lock.yml
@@ -5,7 +5,7 @@
#
# Source: githubnext/agentics/workflows/issue-triage.md@0837fb7b24c3b84ee77fb7c8cfa8735c48be347a
#
-# Effective stop-time: 2025-11-27 03:00:29
+# Effective stop-time: 2025-12-03 20:01:19
#
# Job Dependency Graph:
# ```mermaid
@@ -33,18 +33,29 @@
# add_labels --> update_reaction
# missing_tool --> update_reaction
# ```
+#
+# Pinned GitHub Actions:
+# - actions/checkout@v5 (08c6903cd8c0fde910a37f88322edcfb5dd907a8)
+# https://github.com/actions/checkout/commit/08c6903cd8c0fde910a37f88322edcfb5dd907a8
+# - actions/download-artifact@v5 (634f93cb2916e3fdff6788551b99b062d0335ce0)
+# https://github.com/actions/download-artifact/commit/634f93cb2916e3fdff6788551b99b062d0335ce0
+# - actions/github-script@v8 (ed597411d8f924073f98dfc5c65a23a2325f34cd)
+# https://github.com/actions/github-script/commit/ed597411d8f924073f98dfc5c65a23a2325f34cd
+# - actions/setup-node@v6 (2028fbc5c25fe9cf00d9f06a71cc4710d4507903)
+# https://github.com/actions/setup-node/commit/2028fbc5c25fe9cf00d9f06a71cc4710d4507903
+# - actions/upload-artifact@v4 (ea165f8d65b6e75b540449e92b4886f43607fa02)
+# https://github.com/actions/upload-artifact/commit/ea165f8d65b6e75b540449e92b4886f43607fa02
name: "Agentic Triage"
"on":
- issues:
- types:
- - opened
- - reopened
+ schedule:
+ - cron: 0 0 * * *
+ workflow_dispatch: null
permissions: read-all
concurrency:
- group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}"
+ group: "gh-aw-${{ github.workflow }}"
run-name: "Agentic Triage"
@@ -52,7 +63,7 @@ jobs:
activation:
needs: pre_activation
if: needs.pre_activation.outputs.activated == 'true'
- runs-on: ubuntu-latest
+ runs-on: ubuntu-slim
permissions:
discussions: write
issues: write
@@ -63,24 +74,82 @@ jobs:
comment_url: ${{ steps.react.outputs.comment-url }}
reaction_id: ${{ steps.react.outputs.reaction-id }}
steps:
+ - name: Checkout workflows
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
+ with:
+ sparse-checkout: |
+ .github/workflows
+ sparse-checkout-cone-mode: false
+ fetch-depth: 1
+ persist-credentials: false
- name: Check workflow file timestamps
- run: |
- WORKFLOW_FILE="${GITHUB_WORKSPACE}/.github/workflows/$(basename "$GITHUB_WORKFLOW" .lock.yml).md"
- LOCK_FILE="${GITHUB_WORKSPACE}/.github/workflows/$GITHUB_WORKFLOW"
-
- if [ -f "$WORKFLOW_FILE" ] && [ -f "$LOCK_FILE" ]; then
- if [ "$WORKFLOW_FILE" -nt "$LOCK_FILE" ]; then
- echo "🔴🔴🔴 WARNING: Lock file '$LOCK_FILE' is outdated! The workflow file '$WORKFLOW_FILE' has been modified more recently. Run 'gh aw compile' to regenerate the lock file." >&2
- echo "## ⚠️ Workflow Lock File Warning" >> $GITHUB_STEP_SUMMARY
- echo "🔴🔴🔴 **WARNING**: Lock file \`$LOCK_FILE\` is outdated!" >> $GITHUB_STEP_SUMMARY
- echo "The workflow file \`$WORKFLOW_FILE\` has been modified more recently." >> $GITHUB_STEP_SUMMARY
- echo "Run \`gh aw compile\` to regenerate the lock file." >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- fi
- fi
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
+ env:
+ GH_AW_WORKFLOW_FILE: "issue-triage.lock.yml"
+ with:
+ script: |
+ const fs = require("fs");
+ const path = require("path");
+ async function main() {
+ const workspace = process.env.GITHUB_WORKSPACE;
+ const workflowFile = process.env.GH_AW_WORKFLOW_FILE;
+ if (!workspace) {
+ core.setFailed("Configuration error: GITHUB_WORKSPACE not available.");
+ return;
+ }
+ if (!workflowFile) {
+ core.setFailed("Configuration error: GH_AW_WORKFLOW_FILE not available.");
+ return;
+ }
+ const workflowBasename = path.basename(workflowFile, ".lock.yml");
+ const workflowMdFile = path.join(workspace, ".github", "workflows", `${workflowBasename}.md`);
+ const lockFile = path.join(workspace, ".github", "workflows", workflowFile);
+ core.info(`Checking workflow timestamps:`);
+ core.info(` Source: ${workflowMdFile}`);
+ core.info(` Lock file: ${lockFile}`);
+ let workflowExists = false;
+ let lockExists = false;
+ try {
+ fs.accessSync(workflowMdFile, fs.constants.F_OK);
+ workflowExists = true;
+ } catch (error) {
+ core.info(`Source file does not exist: ${workflowMdFile}`);
+ }
+ try {
+ fs.accessSync(lockFile, fs.constants.F_OK);
+ lockExists = true;
+ } catch (error) {
+ core.info(`Lock file does not exist: ${lockFile}`);
+ }
+ if (!workflowExists || !lockExists) {
+ core.info("Skipping timestamp check - one or both files not found");
+ return;
+ }
+ const workflowStat = fs.statSync(workflowMdFile);
+ const lockStat = fs.statSync(lockFile);
+ const workflowMtime = workflowStat.mtime.getTime();
+ const lockMtime = lockStat.mtime.getTime();
+ core.info(` Source modified: ${workflowStat.mtime.toISOString()}`);
+ core.info(` Lock modified: ${lockStat.mtime.toISOString()}`);
+ if (workflowMtime > lockMtime) {
+ const warningMessage = `🔴🔴🔴 WARNING: Lock file '${lockFile}' is outdated! The workflow file '${workflowMdFile}' has been modified more recently. Run 'gh aw compile' to regenerate the lock file.`;
+ core.error(warningMessage);
+ await core.summary
+ .addRaw("## ⚠️ Workflow Lock File Warning\n\n")
+ .addRaw(`🔴🔴🔴 **WARNING**: Lock file \`${lockFile}\` is outdated!\n\n`)
+ .addRaw(`The workflow file \`${workflowMdFile}\` has been modified more recently.\n\n`)
+ .addRaw("Run `gh aw compile` to regenerate the lock file.\n\n")
+ .write();
+ } else {
+ core.info("✅ Lock file is up to date");
+ }
+ }
+ main().catch(error => {
+ core.setFailed(error instanceof Error ? error.message : String(error));
+ });
- name: Add eyes reaction to the triggering item
id: react
- if: github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment' || (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.repository)
+ if: github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment' || (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.id == github.repository_id)
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
env:
GH_AW_REACTION: eyes
@@ -414,9 +483,9 @@ jobs:
- agent
- detection
if: >
- ((!cancelled()) && (contains(needs.agent.outputs.output_types, 'add_comment'))) && (((github.event.issue.number) ||
- (github.event.pull_request.number)) || (github.event.discussion.number))
- runs-on: ubuntu-latest
+ (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'add_comment'))) &&
+ (((github.event.issue.number) || (github.event.pull_request.number)) || (github.event.discussion.number))
+ runs-on: ubuntu-slim
permissions:
contents: read
discussions: write
@@ -805,9 +874,9 @@ jobs:
- agent
- detection
if: >
- ((!cancelled()) && (contains(needs.agent.outputs.output_types, 'add_labels'))) && ((github.event.issue.number) ||
- (github.event.pull_request.number))
- runs-on: ubuntu-latest
+ (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'add_labels'))) &&
+ ((github.event.issue.number) || (github.event.pull_request.number))
+ runs-on: ubuntu-slim
permissions:
contents: read
issues: write
@@ -1046,6 +1115,8 @@ jobs:
needs: activation
runs-on: ubuntu-latest
permissions: read-all
+ concurrency:
+ group: "gh-aw-copilot-${{ github.workflow }}"
env:
GH_AW_SAFE_OUTPUTS: /tmp/gh-aw/safeoutputs/outputs.jsonl
GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":1},\"add_labels\":{\"max\":5},\"missing_tool\":{}}"
@@ -1055,14 +1126,22 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
+ with:
+ persist-credentials: false
- name: Create gh-aw temp directory
run: |
mkdir -p /tmp/gh-aw/agent
echo "Created /tmp/gh-aw/agent directory for agentic workflow temporary files"
- name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
run: |
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- git config --global user.name "${{ github.workflow }}"
+ git config --global user.name "github-actions[bot]"
+ # Re-authenticate git with GitHub token
+ SERVER_URL="${{ github.server_url }}"
+ SERVER_URL="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL}/${REPO_NAME}.git"
echo "Git configured with standard GitHub Actions identity"
- name: Checkout PR branch
if: |
@@ -1114,15 +1193,15 @@ jobs:
env:
COPILOT_CLI_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }}
- name: Setup Node.js
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
+ uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903
with:
node-version: '24'
- name: Install GitHub Copilot CLI
- run: npm install -g @github/copilot@0.0.351
+ run: npm install -g @github/copilot@0.0.353
- name: Downloading container images
run: |
set -e
- docker pull ghcr.io/github/github-mcp-server:v0.19.1
+ docker pull ghcr.io/github/github-mcp-server:v0.20.1
docker pull mcp/fetch
- name: Setup Safe Outputs Collector MCP
run: |
@@ -1913,6 +1992,13 @@ jobs:
chmod +x /tmp/gh-aw/safeoutputs/mcp-server.cjs
- name: Setup MCPs
+ env:
+ GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_SAFE_OUTPUTS_CONFIG: ${{ toJSON(env.GH_AW_SAFE_OUTPUTS_CONFIG) }}
+ GH_AW_ASSETS_BRANCH: ${{ env.GH_AW_ASSETS_BRANCH }}
+ GH_AW_ASSETS_MAX_SIZE_KB: ${{ env.GH_AW_ASSETS_MAX_SIZE_KB }}
+ GH_AW_ASSETS_ALLOWED_EXTS: ${{ env.GH_AW_ASSETS_ALLOWED_EXTS }}
run: |
mkdir -p /tmp/gh-aw/mcp-config
mkdir -p /home/runner/.copilot
@@ -1932,7 +2018,7 @@ jobs:
"GITHUB_READ_ONLY=1",
"-e",
"GITHUB_TOOLSETS=default",
- "ghcr.io/github/github-mcp-server:v0.19.1"
+ "ghcr.io/github/github-mcp-server:v0.20.1"
],
"tools": ["*"],
"env": {
@@ -1949,7 +2035,9 @@ jobs:
"GH_AW_SAFE_OUTPUTS_CONFIG": "\${GH_AW_SAFE_OUTPUTS_CONFIG}",
"GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}",
"GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}",
- "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}"
+ "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}",
+ "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}",
+ "GITHUB_SERVER_URL": "\${GITHUB_SERVER_URL}"
}
},
"web-fetch": {
@@ -1978,25 +2066,28 @@ jobs:
GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
run: |
mkdir -p $(dirname "$GH_AW_PROMPT")
- cat > $GH_AW_PROMPT << 'PROMPT_EOF'
+ cat > "$GH_AW_PROMPT" << 'PROMPT_EOF'
# Agentic Triage
- You're a triage assistant for GitHub issues. Your task is to analyze issue #${{ github.event.issue.number }} and perform some initial triage tasks related to that issue.
+ You're a triage assistant for GitHub issues. Your task is to analyze issues created in the last 24 hours and perform initial triage tasks for each of them.
- 1. Select appropriate labels for the issue from the provided list.
+ 1. First, use the `list_issues` tool to retrieve all issues created in the last 24 hours. Filter issues by using the `since` parameter with a timestamp from 24 hours ago (calculate: current time minus 24 hours in ISO 8601 format).
- 2. Retrieve the issue content using the `get_issue` tool. If the issue is obviously spam, or generated by bot, or something else that is not an actual issue to be worked on, then add an issue comment to the issue with a one sentence analysis and exit the workflow.
+ 2. For each issue found, perform the following triage tasks:
- 3. Next, use the GitHub tools to gather additional context about the issue:
+ 3. Select appropriate labels for the issue from the provided list.
+
+ 4. Retrieve the issue content using the `get_issue` tool. If the issue is obviously spam, or generated by bot, or something else that is not an actual issue to be worked on, then add an issue comment to the issue with a one sentence analysis and move to the next issue.
+
+ 5. Next, use the GitHub tools to gather additional context about the issue:
- Fetch the list of labels available in this repository. Use 'gh label list' bash command to fetch the labels. This will give you the labels you can use for triaging issues.
- Fetch any comments on the issue using the `get_issue_comments` tool
- - Find similar issues if needed using the `search_issues` tool
- - List the issues to see other open issues in the repository using the `list_issues` tool
+ - **Search for duplicate and related issues**: Use the `search_issues` tool to find similar issues by searching for key terms from the issue title and description. Look for both open and closed issues that might be related or duplicates.
- 4. Analyze the issue content, considering:
+ 6. Analyze the issue content, considering:
- The issue title and description
- The type of issue (bug report, feature request, question, etc.)
@@ -2005,9 +2096,9 @@ jobs:
- User impact
- Components affected
- 5. Write notes, ideas, nudges, resource links, debugging strategies and/or reproduction steps for the team to consider relevant to the issue.
+ 7. Write notes, ideas, nudges, resource links, debugging strategies and/or reproduction steps for the team to consider relevant to the issue.
- 6. Select appropriate labels from the available labels list provided above:
+ 8. Select appropriate labels from the available labels list provided above:
- Choose labels that accurately reflect the issue's nature
- Be specific but comprehensive
@@ -2017,15 +2108,16 @@ jobs:
- Only select labels from the provided list above
- It's okay to not add any labels if none are clearly applicable
- 7. Apply the selected labels:
+ 9. Apply the selected labels:
- Use the `update_issue` tool to apply the labels to the issue
- DO NOT communicate directly with users
- If no labels are clearly applicable, do not apply any labels
- 8. Add an issue comment to the issue with your analysis:
+ 10. Add an issue comment to the issue with your analysis:
- Start with "🎯 Agentic Issue Triage"
- Provide a brief summary of the issue
+ - **If duplicate or related issues were found**, add a section listing them with links (e.g., "### 🔗 Potentially Related Issues" followed by a bullet list of related issues with their titles and links)
- Mention any relevant details that might help the team understand the issue better
- Include any debugging strategies or reproduction steps if applicable
- Suggest resources or links that might be helpful for resolving the issue or learning skills related to the issue or the particular area of the codebase affected by it
@@ -2035,12 +2127,14 @@ jobs:
- If appropriate break the issue down to sub-tasks and write a checklist of things to do.
- Use collapsed-by-default sections in the GitHub markdown to keep the comment tidy. Collapse all sections except the short main summary at the top.
+ 11. After processing all issues, provide a summary of how many issues were triaged. If no issues were created in the last 24 hours, simply note that no new issues needed triage.
+
PROMPT_EOF
- name: Append XPIA security instructions to prompt
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
run: |
- cat >> $GH_AW_PROMPT << 'PROMPT_EOF'
+ cat >> "$GH_AW_PROMPT" << 'PROMPT_EOF'
---
@@ -2072,7 +2166,7 @@ jobs:
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
run: |
- cat >> $GH_AW_PROMPT << 'PROMPT_EOF'
+ cat >> "$GH_AW_PROMPT" << 'PROMPT_EOF'
---
@@ -2085,7 +2179,7 @@ jobs:
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
run: |
- cat >> $GH_AW_PROMPT << 'PROMPT_EOF'
+ cat >> "$GH_AW_PROMPT" << 'PROMPT_EOF'
---
@@ -2110,7 +2204,7 @@ jobs:
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
run: |
- cat >> $GH_AW_PROMPT << 'PROMPT_EOF'
+ cat >> "$GH_AW_PROMPT" << 'PROMPT_EOF'
---
@@ -2179,14 +2273,14 @@ jobs:
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
run: |
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "Generated Prompt
" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo '```markdown' >> $GITHUB_STEP_SUMMARY
- cat $GH_AW_PROMPT >> $GITHUB_STEP_SUMMARY
- echo '```' >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo " " >> $GITHUB_STEP_SUMMARY
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "Generated Prompt
" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo '```markdown' >> "$GITHUB_STEP_SUMMARY"
+ cat "$GH_AW_PROMPT" >> "$GITHUB_STEP_SUMMARY"
+ echo '```' >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo " " >> "$GITHUB_STEP_SUMMARY"
- name: Upload prompt
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
@@ -2194,13 +2288,6 @@ jobs:
name: prompt.txt
path: /tmp/gh-aw/aw-prompts/prompt.txt
if-no-files-found: warn
- - name: Capture agent version
- run: |
- VERSION_OUTPUT=$(copilot --version 2>&1 || echo "unknown")
- # Extract semantic version pattern (e.g., 1.2.3, v1.2.3-beta)
- CLEAN_VERSION=$(echo "$VERSION_OUTPUT" | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?' | head -n1 || echo "unknown")
- echo "AGENT_VERSION=$CLEAN_VERSION" >> $GITHUB_ENV
- echo "Agent version: $VERSION_OUTPUT"
- name: Generate agentic run info
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
with:
@@ -2212,7 +2299,7 @@ jobs:
engine_name: "GitHub Copilot CLI",
model: "",
version: "",
- agent_version: process.env.AGENT_VERSION || "",
+ agent_version: "0.0.353",
workflow_name: "Agentic Triage",
experimental: false,
supports_tools_allowlist: true,
@@ -2226,6 +2313,9 @@ jobs:
actor: context.actor,
event_name: context.eventName,
staged: false,
+ steps: {
+ firewall: ""
+ },
created_at: new Date().toISOString()
};
@@ -2262,9 +2352,12 @@ jobs:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":1},\"add_labels\":{\"max\":5},\"missing_tool\":{}}"
+ GITHUB_HEAD_REF: ${{ github.head_ref }}
GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ GITHUB_REF_NAME: ${{ github.ref_name }}
GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }}
GITHUB_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }}
+ GITHUB_WORKSPACE: ${{ github.workspace }}
XDG_CONFIG_HOME: /home/runner
- name: Redact secrets in logs
if: always()
@@ -2399,71 +2492,135 @@ jobs:
script: |
async function main() {
const fs = require("fs");
- const maxBodyLength = 65000;
- function sanitizeContent(content, maxLength) {
- if (!content || typeof content !== "string") {
- return "";
- }
- const allowedDomainsEnv = process.env.GH_AW_ALLOWED_DOMAINS;
- const defaultAllowedDomains = ["github.com", "github.io", "githubusercontent.com", "githubassets.com", "github.dev", "codespaces.new"];
- const allowedDomains = allowedDomainsEnv
- ? allowedDomainsEnv
- .split(",")
- .map(d => d.trim())
- .filter(d => d)
- : defaultAllowedDomains;
- let sanitized = content;
- sanitized = neutralizeMentions(sanitized);
- sanitized = removeXmlComments(sanitized);
- sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
- sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
- sanitized = sanitizeUrlProtocols(sanitized);
- sanitized = sanitizeUrlDomains(sanitized);
- const lines = sanitized.split("\n");
- const maxLines = 65000;
- maxLength = maxLength || 524288;
- if (lines.length > maxLines) {
- const truncationMsg = "\n[Content truncated due to line count]";
- const truncatedLines = lines.slice(0, maxLines).join("\n") + truncationMsg;
- if (truncatedLines.length > maxLength) {
- sanitized = truncatedLines.substring(0, maxLength - truncationMsg.length) + truncationMsg;
- } else {
- sanitized = truncatedLines;
- }
- } else if (sanitized.length > maxLength) {
- sanitized = sanitized.substring(0, maxLength) + "\n[Content truncated due to length]";
- }
- sanitized = neutralizeBotTriggers(sanitized);
- return sanitized.trim();
- function sanitizeUrlDomains(s) {
- return s.replace(/\bhttps:\/\/[^\s\])}'"<>&\x00-\x1f,;]+/gi, match => {
- const urlAfterProtocol = match.slice(8);
- const hostname = urlAfterProtocol.split(/[\/:\?#]/)[0].toLowerCase();
- const isAllowed = allowedDomains.some(allowedDomain => {
- const normalizedAllowed = allowedDomain.toLowerCase();
- return hostname === normalizedAllowed || hostname.endsWith("." + normalizedAllowed);
- });
- return isAllowed ? match : "(redacted)";
- });
- }
- function sanitizeUrlProtocols(s) {
- return s.replace(/\b(\w+):\/\/[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
- return protocol.toLowerCase() === "https" ? match : "(redacted)";
- });
- }
- function neutralizeMentions(s) {
- return s.replace(
- /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
- (_m, p1, p2) => `${p1}\`@${p2}\``
- );
- }
- function removeXmlComments(s) {
- return s.replace(//g, "").replace(//g, "");
- }
- function neutralizeBotTriggers(s) {
- return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi, (match, action, ref) => `\`${action} #${ref}\``);
- }
+ function sanitizeContent(content, maxLength) {
+ if (!content || typeof content !== "string") {
+ return "";
}
+ const allowedDomainsEnv = process.env.GH_AW_ALLOWED_DOMAINS;
+ const defaultAllowedDomains = ["github.com", "github.io", "githubusercontent.com", "githubassets.com", "github.dev", "codespaces.new"];
+ const allowedDomains = allowedDomainsEnv
+ ? allowedDomainsEnv
+ .split(",")
+ .map(d => d.trim())
+ .filter(d => d)
+ : defaultAllowedDomains;
+ let sanitized = content;
+ sanitized = neutralizeCommands(sanitized);
+ sanitized = neutralizeMentions(sanitized);
+ sanitized = removeXmlComments(sanitized);
+ sanitized = convertXmlTags(sanitized);
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitizeUrlProtocols(sanitized);
+ sanitized = sanitizeUrlDomains(sanitized);
+ const lines = sanitized.split("\n");
+ const maxLines = 65000;
+ maxLength = maxLength || 524288;
+ if (lines.length > maxLines) {
+ const truncationMsg = "\n[Content truncated due to line count]";
+ const truncatedLines = lines.slice(0, maxLines).join("\n") + truncationMsg;
+ if (truncatedLines.length > maxLength) {
+ sanitized = truncatedLines.substring(0, maxLength - truncationMsg.length) + truncationMsg;
+ } else {
+ sanitized = truncatedLines;
+ }
+ } else if (sanitized.length > maxLength) {
+ sanitized = sanitized.substring(0, maxLength) + "\n[Content truncated due to length]";
+ }
+ sanitized = neutralizeBotTriggers(sanitized);
+ return sanitized.trim();
+ function sanitizeUrlDomains(s) {
+ s = s.replace(/\bhttps:\/\/([^\s\])}'"<>&\x00-\x1f,;]+)/gi, (match, rest) => {
+ const hostname = rest.split(/[\/:\?#]/)[0].toLowerCase();
+ const isAllowed = allowedDomains.some(allowedDomain => {
+ const normalizedAllowed = allowedDomain.toLowerCase();
+ return hostname === normalizedAllowed || hostname.endsWith("." + normalizedAllowed);
+ });
+ if (isAllowed) {
+ return match;
+ }
+ const domain = hostname;
+ const truncated = domain.length > 12 ? domain.substring(0, 12) + "..." : domain;
+ core.info(`Redacted URL: ${truncated}`);
+ core.debug(`Redacted URL (full): ${match}`);
+ const urlParts = match.split(/([?])/);
+ let result = "(redacted)";
+ for (let i = 1; i < urlParts.length; i++) {
+ if (urlParts[i].match(/^[?]$/)) {
+ result += urlParts[i];
+ } else {
+ result += sanitizeUrlDomains(urlParts[i]);
+ }
+ }
+ return result;
+ });
+ return s;
+ }
+ function sanitizeUrlProtocols(s) {
+ return s.replace(/(?&\x00-\x1f]+/g, (match, protocol) => {
+ if (protocol.toLowerCase() === "https") {
+ return match;
+ }
+ if (match.includes("::")) {
+ return match;
+ }
+ if (match.includes("://")) {
+ const domainMatch = match.match(/^[^:]+:\/\/([^\/\s?#]+)/);
+ const domain = domainMatch ? domainMatch[1] : match;
+ const truncated = domain.length > 12 ? domain.substring(0, 12) + "..." : domain;
+ core.info(`Redacted URL: ${truncated}`);
+ core.debug(`Redacted URL (full): ${match}`);
+ return "(redacted)";
+ }
+ const dangerousProtocols = ["javascript", "data", "vbscript", "file", "about", "mailto", "tel", "ssh", "ftp"];
+ if (dangerousProtocols.includes(protocol.toLowerCase())) {
+ const truncated = match.length > 12 ? match.substring(0, 12) + "..." : match;
+ core.info(`Redacted URL: ${truncated}`);
+ core.debug(`Redacted URL (full): ${match}`);
+ return "(redacted)";
+ }
+ return match;
+ });
+ }
+ function neutralizeCommands(s) {
+ const commandName = process.env.GH_AW_COMMAND;
+ if (!commandName) {
+ return s;
+ }
+ const escapedCommand = commandName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ return s.replace(new RegExp(`^(\\s*)/(${escapedCommand})\\b`, "i"), "$1`/$2`");
+ }
+ function neutralizeMentions(s) {
+ return s.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ }
+ function removeXmlComments(s) {
+ return s.replace(//g, "").replace(//g, "");
+ }
+ function convertXmlTags(s) {
+ const allowedTags = ["details", "summary", "code", "em", "b"];
+ s = s.replace(//g, (match, content) => {
+ const convertedContent = content.replace(/<(\/?[A-Za-z][A-Za-z0-9]*(?:[^>]*?))>/g, "($1)");
+ return `(![CDATA[${convertedContent}]])`;
+ });
+ return s.replace(/<(\/?[A-Za-z!][^>]*?)>/g, (match, tagContent) => {
+ const tagNameMatch = tagContent.match(/^\/?\s*([A-Za-z][A-Za-z0-9]*)/);
+ if (tagNameMatch) {
+ const tagName = tagNameMatch[1].toLowerCase();
+ if (allowedTags.includes(tagName)) {
+ return match;
+ }
+ }
+ return `(${tagContent})`;
+ });
+ }
+ function neutralizeBotTriggers(s) {
+ return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi, (match, action, ref) => `\`${action} #${ref}\``);
+ }
+ }
+ const maxBodyLength = 65000;
function getMaxAllowedForType(itemType, config) {
const itemConfig = config?.[itemType];
if (itemConfig && typeof itemConfig === "object" && "max" in itemConfig && itemConfig.max) {
@@ -4295,7 +4452,9 @@ jobs:
detection:
needs: agent
runs-on: ubuntu-latest
- permissions: read-all
+ permissions: {}
+ concurrency:
+ group: "gh-aw-copilot-${{ github.workflow }}"
timeout-minutes: 10
steps:
- name: Download prompt artifact
@@ -4444,11 +4603,11 @@ jobs:
env:
COPILOT_CLI_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }}
- name: Setup Node.js
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
+ uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903
with:
node-version: '24'
- name: Install GitHub Copilot CLI
- run: npm install -g @github/copilot@0.0.351
+ run: npm install -g @github/copilot@0.0.353
- name: Execute GitHub Copilot CLI
id: agentic_execution
# Copilot CLI tool arguments (sorted):
@@ -4471,8 +4630,11 @@ jobs:
env:
COPILOT_AGENT_RUNNER_TYPE: STANDALONE
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GITHUB_HEAD_REF: ${{ github.head_ref }}
+ GITHUB_REF_NAME: ${{ github.ref_name }}
GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }}
GITHUB_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }}
+ GITHUB_WORKSPACE: ${{ github.workspace }}
XDG_CONFIG_HOME: /home/runner
- name: Parse threat detection results
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
@@ -4522,8 +4684,8 @@ jobs:
needs:
- agent
- detection
- if: (!cancelled()) && (contains(needs.agent.outputs.output_types, 'missing_tool'))
- runs-on: ubuntu-latest
+ if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'missing_tool'))
+ runs-on: ubuntu-slim
permissions:
contents: read
timeout-minutes: 5
@@ -4651,89 +4813,15 @@ jobs:
});
pre_activation:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-slim
outputs:
- activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_stop_time.outputs.stop_time_ok == 'true') }}
+ activated: ${{ steps.check_stop_time.outputs.stop_time_ok == 'true' }}
steps:
- - name: Check team membership for workflow
- id: check_membership
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
- env:
- GH_AW_REQUIRED_ROLES: admin,maintainer,write
- with:
- script: |
- async function main() {
- const { eventName } = context;
- const actor = context.actor;
- const { owner, repo } = context.repo;
- const requiredPermissionsEnv = process.env.GH_AW_REQUIRED_ROLES;
- const requiredPermissions = requiredPermissionsEnv ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") : [];
- if (eventName === "workflow_dispatch") {
- const hasWriteRole = requiredPermissions.includes("write");
- if (hasWriteRole) {
- core.info(`✅ Event ${eventName} does not require validation (write role allowed)`);
- core.setOutput("is_team_member", "true");
- core.setOutput("result", "safe_event");
- return;
- }
- core.info(`Event ${eventName} requires validation (write role not allowed)`);
- }
- const safeEvents = ["workflow_run", "schedule"];
- if (safeEvents.includes(eventName)) {
- core.info(`✅ Event ${eventName} does not require validation`);
- core.setOutput("is_team_member", "true");
- core.setOutput("result", "safe_event");
- return;
- }
- if (!requiredPermissions || requiredPermissions.length === 0) {
- core.warning("❌ Configuration error: Required permissions not specified. Contact repository administrator.");
- core.setOutput("is_team_member", "false");
- core.setOutput("result", "config_error");
- core.setOutput("error_message", "Configuration error: Required permissions not specified");
- return;
- }
- try {
- core.info(`Checking if user '${actor}' has required permissions for ${owner}/${repo}`);
- core.info(`Required permissions: ${requiredPermissions.join(", ")}`);
- const repoPermission = await github.rest.repos.getCollaboratorPermissionLevel({
- owner: owner,
- repo: repo,
- username: actor,
- });
- const permission = repoPermission.data.permission;
- core.info(`Repository permission level: ${permission}`);
- for (const requiredPerm of requiredPermissions) {
- if (permission === requiredPerm || (requiredPerm === "maintainer" && permission === "maintain")) {
- core.info(`✅ User has ${permission} access to repository`);
- core.setOutput("is_team_member", "true");
- core.setOutput("result", "authorized");
- core.setOutput("user_permission", permission);
- return;
- }
- }
- core.warning(`User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}`);
- core.setOutput("is_team_member", "false");
- core.setOutput("result", "insufficient_permissions");
- core.setOutput("user_permission", permission);
- core.setOutput(
- "error_message",
- `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}`
- );
- } catch (repoError) {
- const errorMessage = repoError instanceof Error ? repoError.message : String(repoError);
- core.warning(`Repository permission check failed: ${errorMessage}`);
- core.setOutput("is_team_member", "false");
- core.setOutput("result", "api_error");
- core.setOutput("error_message", `Repository permission check failed: ${errorMessage}`);
- return;
- }
- }
- await main();
- name: Check stop-time limit
id: check_stop_time
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
env:
- GH_AW_STOP_TIME: 2025-11-27 03:00:29
+ GH_AW_STOP_TIME: 2025-12-03 20:01:19
GH_AW_WORKFLOW_NAME: "Agentic Triage"
with:
script: |
@@ -4776,7 +4864,7 @@ jobs:
if: >
(((((always()) && (needs.agent.result != 'skipped')) && (needs.activation.outputs.comment_id)) && (!contains(needs.agent.outputs.output_types, 'add_comment'))) &&
(!contains(needs.agent.outputs.output_types, 'create_pull_request'))) && (!contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch'))
- runs-on: ubuntu-latest
+ runs-on: ubuntu-slim
permissions:
contents: read
discussions: write
diff --git a/.github/workflows/issue-triage.md b/.github/workflows/issue-triage.md
index 2b4739988a..087f009106 100644
--- a/.github/workflows/issue-triage.md
+++ b/.github/workflows/issue-triage.md
@@ -1,7 +1,8 @@
---
on:
- issues:
- types: [opened, reopened]
+ schedule:
+ - cron: '0 0 * * *' # Run daily at midnight UTC
+ workflow_dispatch: # Enable manual trigger
stop-after: +30d # workflow will no longer trigger after 30 days. Remove this and recompile to run indefinitely
reaction: eyes
@@ -25,20 +26,23 @@ source: githubnext/agentics/workflows/issue-triage.md@0837fb7b24c3b84ee77fb7c8cf
-You're a triage assistant for GitHub issues. Your task is to analyze issue #${{ github.event.issue.number }} and perform some initial triage tasks related to that issue.
+You're a triage assistant for GitHub issues. Your task is to analyze issues created in the last 24 hours and perform initial triage tasks for each of them.
-1. Select appropriate labels for the issue from the provided list.
+1. First, use the `list_issues` tool to retrieve all issues created in the last 24 hours. Filter issues by using the `since` parameter with a timestamp from 24 hours ago (calculate: current time minus 24 hours in ISO 8601 format).
-2. Retrieve the issue content using the `get_issue` tool. If the issue is obviously spam, or generated by bot, or something else that is not an actual issue to be worked on, then add an issue comment to the issue with a one sentence analysis and exit the workflow.
+2. For each issue found, perform the following triage tasks:
-3. Next, use the GitHub tools to gather additional context about the issue:
+3. Select appropriate labels for the issue from the provided list.
+
+4. Retrieve the issue content using the `get_issue` tool. If the issue is obviously spam, or generated by bot, or something else that is not an actual issue to be worked on, then add an issue comment to the issue with a one sentence analysis and move to the next issue.
+
+5. Next, use the GitHub tools to gather additional context about the issue:
- Fetch the list of labels available in this repository. Use 'gh label list' bash command to fetch the labels. This will give you the labels you can use for triaging issues.
- Fetch any comments on the issue using the `get_issue_comments` tool
- - Find similar issues if needed using the `search_issues` tool
- - List the issues to see other open issues in the repository using the `list_issues` tool
+ - **Search for duplicate and related issues**: Use the `search_issues` tool to find similar issues by searching for key terms from the issue title and description. Look for both open and closed issues that might be related or duplicates.
-4. Analyze the issue content, considering:
+6. Analyze the issue content, considering:
- The issue title and description
- The type of issue (bug report, feature request, question, etc.)
@@ -47,9 +51,9 @@ You're a triage assistant for GitHub issues. Your task is to analyze issue #${{
- User impact
- Components affected
-5. Write notes, ideas, nudges, resource links, debugging strategies and/or reproduction steps for the team to consider relevant to the issue.
+7. Write notes, ideas, nudges, resource links, debugging strategies and/or reproduction steps for the team to consider relevant to the issue.
-6. Select appropriate labels from the available labels list provided above:
+8. Select appropriate labels from the available labels list provided above:
- Choose labels that accurately reflect the issue's nature
- Be specific but comprehensive
@@ -59,15 +63,16 @@ You're a triage assistant for GitHub issues. Your task is to analyze issue #${{
- Only select labels from the provided list above
- It's okay to not add any labels if none are clearly applicable
-7. Apply the selected labels:
+9. Apply the selected labels:
- Use the `update_issue` tool to apply the labels to the issue
- DO NOT communicate directly with users
- If no labels are clearly applicable, do not apply any labels
-8. Add an issue comment to the issue with your analysis:
+10. Add an issue comment to the issue with your analysis:
- Start with "🎯 Agentic Issue Triage"
- Provide a brief summary of the issue
+ - **If duplicate or related issues were found**, add a section listing them with links (e.g., "### 🔗 Potentially Related Issues" followed by a bullet list of related issues with their titles and links)
- Mention any relevant details that might help the team understand the issue better
- Include any debugging strategies or reproduction steps if applicable
- Suggest resources or links that might be helpful for resolving the issue or learning skills related to the issue or the particular area of the codebase affected by it
@@ -76,3 +81,5 @@ You're a triage assistant for GitHub issues. Your task is to analyze issue #${{
- If you have any debugging strategies, include them in the comment
- If appropriate break the issue down to sub-tasks and write a checklist of things to do.
- Use collapsed-by-default sections in the GitHub markdown to keep the comment tidy. Collapse all sections except the short main summary at the top.
+
+11. After processing all issues, provide a summary of how many issues were triaged. If no issues were created in the last 24 hours, simply note that no new issues needed triage.
diff --git a/README.md b/README.md
index 1f24d5c5f2..50c1ed399b 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-> We just announced Transactions API for Appwrite Databases - [Learn more](https://appwrite.io/blog/post/announcing-transactions-api)
+> We just announced DB operators for Appwrite Databases - [Learn more](https://appwrite.io/blog/post/announcing-db-operators)
> Appwrite Cloud is now Generally Available - [Learn more](https://appwrite.io/cloud-ga)
diff --git a/app/config/collections/common.php b/app/config/collections/common.php
index 6de7eb224b..d8e1c1699a 100644
--- a/app/config/collections/common.php
+++ b/app/config/collections/common.php
@@ -364,6 +364,61 @@ return [
'array' => false,
'filters' => ['datetime'],
],
+ [
+ '$id' => ID::custom('emailCanonical'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 320,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('emailIsFree'),
+ 'type' => Database::VAR_BOOLEAN,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('emailIsDisposable'),
+ 'type' => Database::VAR_BOOLEAN,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('emailIsCorporate'),
+ 'type' => Database::VAR_BOOLEAN,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('emailIsCanonical'),
+ 'type' => Database::VAR_BOOLEAN,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
],
'indexes' => [
[
diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php
index bf0cee3527..dae0337dc9 100644
--- a/app/config/collections/projects.php
+++ b/app/config/collections/projects.php
@@ -2345,7 +2345,7 @@ return [
'$id' => ID::custom('errors'),
'type' => Database::VAR_STRING,
'format' => '',
- 'size' => 65535,
+ 'size' => 1_000_000,
'signed' => true,
'required' => true,
'default' => null,
diff --git a/app/config/frameworks.php b/app/config/frameworks.php
index 0ab4a8a7db..47e26ac91e 100644
--- a/app/config/frameworks.php
+++ b/app/config/frameworks.php
@@ -273,7 +273,7 @@ return [
'key' => 'flutter',
'name' => 'Flutter',
'screenshotSleep' => 5000,
- 'buildRuntime' => 'flutter-3.29',
+ 'buildRuntime' => 'flutter-3.35',
'runtimes' => getVersions($templateRuntimes['FLUTTER']['versions'], 'flutter'),
'adapters' => [
'static' => [
@@ -282,6 +282,7 @@ return [
'installCommand' => 'flutter pub get',
'outputDirectory' => './build/web',
'startCommand' => 'bash helpers/server.sh',
+ 'fallbackFile' => 'index.html'
],
],
],
diff --git a/app/config/platforms.php b/app/config/platforms.php
index 361ec6b935..2b5c107648 100644
--- a/app/config/platforms.php
+++ b/app/config/platforms.php
@@ -60,7 +60,7 @@ return [
[
'key' => 'flutter',
'name' => 'Flutter',
- 'version' => '20.3.0',
+ 'version' => '20.3.1',
'url' => 'https://github.com/appwrite/sdk-for-flutter',
'package' => 'https://pub.dev/packages/appwrite',
'enabled' => true,
@@ -226,7 +226,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
- 'version' => '11.1.0',
+ 'version' => '11.1.1',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
@@ -281,7 +281,7 @@ return [
[
'key' => 'php',
'name' => 'PHP',
- 'version' => '17.5.0',
+ 'version' => '18.0.1',
'url' => 'https://github.com/appwrite/sdk-for-php',
'package' => 'https://packagist.org/packages/appwrite/appwrite',
'enabled' => true,
@@ -376,7 +376,7 @@ return [
[
'key' => 'dart',
'name' => 'Dart',
- 'version' => '19.3.0',
+ 'version' => '19.4.0',
'url' => 'https://github.com/appwrite/sdk-for-dart',
'package' => 'https://pub.dev/packages/dart_appwrite',
'enabled' => true,
diff --git a/app/config/specs/open-api3-1.8.x-console.json b/app/config/specs/open-api3-1.8.x-console.json
index d7c156cdcd..1d831b7fd2 100644
--- a/app/config/specs/open-api3-1.8.x-console.json
+++ b/app/config/specs/open-api3-1.8.x-console.json
@@ -13154,6 +13154,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -13180,7 +13181,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -13795,6 +13797,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -13821,7 +13824,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -31087,6 +31091,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -31113,7 +31118,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -31734,6 +31740,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -31760,7 +31767,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
diff --git a/app/config/specs/open-api3-1.8.x-server.json b/app/config/specs/open-api3-1.8.x-server.json
index 64e119e68a..8fc49b1db5 100644
--- a/app/config/specs/open-api3-1.8.x-server.json
+++ b/app/config/specs/open-api3-1.8.x-server.json
@@ -12176,6 +12176,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -12202,7 +12203,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -12578,6 +12580,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -12604,7 +12607,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -21827,6 +21831,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -21853,7 +21858,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -22246,6 +22252,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -22272,7 +22279,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json
index d7c156cdcd..03b60a0e10 100644
--- a/app/config/specs/open-api3-latest-console.json
+++ b/app/config/specs/open-api3-latest-console.json
@@ -13154,6 +13154,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -13180,7 +13181,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -13795,6 +13797,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -13821,7 +13824,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -14410,10 +14414,22 @@
"description": "Path to function code in the template repo.",
"x-example": ""
},
- "version": {
+ "type": {
"type": "string",
- "description": "Version (tag) for the repo linked to the function template.",
- "x-example": ""
+ "description": "Type for the reference provided. Can be commit, branch, or tag",
+ "x-example": "commit",
+ "enum": [
+ "commit",
+ "branch",
+ "tag"
+ ],
+ "x-enum-name": null,
+ "x-enum-keys": []
+ },
+ "reference": {
+ "type": "string",
+ "description": "Reference value, can be a commit hash, branch name, or release tag",
+ "x-example": ""
},
"activate": {
"type": "boolean",
@@ -14425,7 +14441,8 @@
"repository",
"owner",
"rootDirectory",
- "version"
+ "type",
+ "reference"
]
}
}
@@ -31087,6 +31104,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -31113,7 +31131,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -31734,6 +31753,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -31760,7 +31780,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -32312,10 +32333,22 @@
"description": "Path to site code in the template repo.",
"x-example": ""
},
- "version": {
+ "type": {
"type": "string",
- "description": "Version (tag) for the repo linked to the site template.",
- "x-example": ""
+ "description": "Type for the reference provided. Can be commit, branch, or tag",
+ "x-example": "branch",
+ "enum": [
+ "branch",
+ "commit",
+ "tag"
+ ],
+ "x-enum-name": null,
+ "x-enum-keys": []
+ },
+ "reference": {
+ "type": "string",
+ "description": "Reference value, can be a commit hash, branch name, or release tag",
+ "x-example": ""
},
"activate": {
"type": "boolean",
@@ -32327,7 +32360,8 @@
"repository",
"owner",
"rootDirectory",
- "version"
+ "type",
+ "reference"
]
}
}
diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json
index 64e119e68a..b11c69442b 100644
--- a/app/config/specs/open-api3-latest-server.json
+++ b/app/config/specs/open-api3-latest-server.json
@@ -12176,6 +12176,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -12202,7 +12203,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -12578,6 +12580,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -12604,7 +12607,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -13199,10 +13203,22 @@
"description": "Path to function code in the template repo.",
"x-example": ""
},
- "version": {
+ "type": {
"type": "string",
- "description": "Version (tag) for the repo linked to the function template.",
- "x-example": ""
+ "description": "Type for the reference provided. Can be commit, branch, or tag",
+ "x-example": "commit",
+ "enum": [
+ "commit",
+ "branch",
+ "tag"
+ ],
+ "x-enum-name": null,
+ "x-enum-keys": []
+ },
+ "reference": {
+ "type": "string",
+ "description": "Reference value, can be a commit hash, branch name, or release tag",
+ "x-example": ""
},
"activate": {
"type": "boolean",
@@ -13214,7 +13230,8 @@
"repository",
"owner",
"rootDirectory",
- "version"
+ "type",
+ "reference"
]
}
}
@@ -21827,6 +21844,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -21853,7 +21871,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -22246,6 +22265,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -22272,7 +22292,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -22830,10 +22851,22 @@
"description": "Path to site code in the template repo.",
"x-example": ""
},
- "version": {
+ "type": {
"type": "string",
- "description": "Version (tag) for the repo linked to the site template.",
- "x-example": ""
+ "description": "Type for the reference provided. Can be commit, branch, or tag",
+ "x-example": "branch",
+ "enum": [
+ "branch",
+ "commit",
+ "tag"
+ ],
+ "x-enum-name": null,
+ "x-enum-keys": []
+ },
+ "reference": {
+ "type": "string",
+ "description": "Reference value, can be a commit hash, branch name, or release tag",
+ "x-example": ""
},
"activate": {
"type": "boolean",
@@ -22845,7 +22878,8 @@
"repository",
"owner",
"rootDirectory",
- "version"
+ "type",
+ "reference"
]
}
}
diff --git a/app/config/specs/swagger2-1.8.x-console.json b/app/config/specs/swagger2-1.8.x-console.json
index 5f05314818..be829c0de0 100644
--- a/app/config/specs/swagger2-1.8.x-console.json
+++ b/app/config/specs/swagger2-1.8.x-console.json
@@ -13090,6 +13090,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -13116,7 +13117,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -13732,6 +13734,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -13758,7 +13761,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -31189,6 +31193,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -31215,7 +31220,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -31839,6 +31845,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -31865,7 +31872,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
diff --git a/app/config/specs/swagger2-1.8.x-server.json b/app/config/specs/swagger2-1.8.x-server.json
index 15606799ea..af417d5788 100644
--- a/app/config/specs/swagger2-1.8.x-server.json
+++ b/app/config/specs/swagger2-1.8.x-server.json
@@ -12127,6 +12127,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -12153,7 +12154,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -12542,6 +12544,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -12568,7 +12571,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -21967,6 +21971,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -21993,7 +21998,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -22399,6 +22405,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -22425,7 +22432,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json
index 5f05314818..611cbf1e1d 100644
--- a/app/config/specs/swagger2-latest-console.json
+++ b/app/config/specs/swagger2-latest-console.json
@@ -13090,6 +13090,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -13116,7 +13117,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -13732,6 +13734,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -13758,7 +13761,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -14351,11 +14355,24 @@
"default": null,
"x-example": ""
},
- "version": {
+ "type": {
"type": "string",
- "description": "Version (tag) for the repo linked to the function template.",
+ "description": "Type for the reference provided. Can be commit, branch, or tag",
"default": null,
- "x-example": ""
+ "x-example": "commit",
+ "enum": [
+ "commit",
+ "branch",
+ "tag"
+ ],
+ "x-enum-name": null,
+ "x-enum-keys": []
+ },
+ "reference": {
+ "type": "string",
+ "description": "Reference value, can be a commit hash, branch name, or release tag",
+ "default": null,
+ "x-example": ""
},
"activate": {
"type": "boolean",
@@ -14368,7 +14385,8 @@
"repository",
"owner",
"rootDirectory",
- "version"
+ "type",
+ "reference"
]
}
}
@@ -31189,6 +31207,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -31215,7 +31234,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -31839,6 +31859,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -31865,7 +31886,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -32415,11 +32437,24 @@
"default": null,
"x-example": ""
},
- "version": {
+ "type": {
"type": "string",
- "description": "Version (tag) for the repo linked to the site template.",
+ "description": "Type for the reference provided. Can be commit, branch, or tag",
"default": null,
- "x-example": ""
+ "x-example": "branch",
+ "enum": [
+ "branch",
+ "commit",
+ "tag"
+ ],
+ "x-enum-name": null,
+ "x-enum-keys": []
+ },
+ "reference": {
+ "type": "string",
+ "description": "Reference value, can be a commit hash, branch name, or release tag",
+ "default": null,
+ "x-example": ""
},
"activate": {
"type": "boolean",
@@ -32432,7 +32467,8 @@
"repository",
"owner",
"rootDirectory",
- "version"
+ "type",
+ "reference"
]
}
}
diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json
index 15606799ea..e48f00475a 100644
--- a/app/config/specs/swagger2-latest-server.json
+++ b/app/config/specs/swagger2-latest-server.json
@@ -12127,6 +12127,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -12153,7 +12154,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -12542,6 +12544,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -12568,7 +12571,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -13167,11 +13171,24 @@
"default": null,
"x-example": ""
},
- "version": {
+ "type": {
"type": "string",
- "description": "Version (tag) for the repo linked to the function template.",
+ "description": "Type for the reference provided. Can be commit, branch, or tag",
"default": null,
- "x-example": ""
+ "x-example": "commit",
+ "enum": [
+ "commit",
+ "branch",
+ "tag"
+ ],
+ "x-enum-name": null,
+ "x-enum-keys": []
+ },
+ "reference": {
+ "type": "string",
+ "description": "Reference value, can be a commit hash, branch name, or release tag",
+ "default": null,
+ "x-example": ""
},
"activate": {
"type": "boolean",
@@ -13184,7 +13201,8 @@
"repository",
"owner",
"rootDirectory",
- "version"
+ "type",
+ "reference"
]
}
}
@@ -21967,6 +21985,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -21993,7 +22012,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -22399,6 +22419,7 @@
"dart-3.3",
"dart-3.5",
"dart-3.8",
+ "dart-3.9",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@@ -22425,7 +22446,8 @@
"flutter-3.24",
"flutter-3.27",
"flutter-3.29",
- "flutter-3.32"
+ "flutter-3.32",
+ "flutter-3.35"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -22981,11 +23003,24 @@
"default": null,
"x-example": ""
},
- "version": {
+ "type": {
"type": "string",
- "description": "Version (tag) for the repo linked to the site template.",
+ "description": "Type for the reference provided. Can be commit, branch, or tag",
"default": null,
- "x-example": ""
+ "x-example": "branch",
+ "enum": [
+ "branch",
+ "commit",
+ "tag"
+ ],
+ "x-enum-name": null,
+ "x-enum-keys": []
+ },
+ "reference": {
+ "type": "string",
+ "description": "Reference value, can be a commit hash, branch name, or release tag",
+ "default": null,
+ "x-example": ""
},
"activate": {
"type": "boolean",
@@ -22998,7 +23033,8 @@
"repository",
"owner",
"rootDirectory",
- "version"
+ "type",
+ "reference"
]
}
}
diff --git a/app/config/template-runtimes.php b/app/config/template-runtimes.php
index d1bb1a5b6a..04eaba2c44 100644
--- a/app/config/template-runtimes.php
+++ b/app/config/template-runtimes.php
@@ -14,7 +14,7 @@ return [
],
'DART' => [
'name' => 'dart',
- 'versions' => ['3.8', '3.5', '3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16']
+ 'versions' => ['3.9', '3.8', '3.5', '3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16']
],
'GO' => [
'name' => 'go',
@@ -38,6 +38,6 @@ return [
],
'FLUTTER' => [
'name' => 'flutter',
- 'versions' => ['3.32', '3.24']
+ 'versions' => ['3.35', '3.32', '3.24']
],
];
diff --git a/app/config/templates/site.php b/app/config/templates/site.php
index e552a6b9ac..c8bb019123 100644
--- a/app/config/templates/site.php
+++ b/app/config/templates/site.php
@@ -24,6 +24,7 @@ class UseCases
public const ECOMMERCE = 'ecommerce';
public const DOCUMENTATION = 'documentation';
public const BLOG = 'blog';
+ public const AI = 'artificial intelligence';
}
const TEMPLATE_FRAMEWORKS = [
@@ -83,7 +84,7 @@ const TEMPLATE_FRAMEWORKS = [
'installCommand' => '',
'buildCommand' => 'flutter build web',
'outputDirectory' => './build/web',
- 'buildRuntime' => 'flutter-3.29',
+ 'buildRuntime' => 'flutter-3.35',
'adapter' => 'static',
'fallbackFile' => '',
],
@@ -970,7 +971,7 @@ return [
'name' => 'TanStack Start starter',
'useCases' => [UseCases::STARTER],
'tagline' => 'Simple TanStack Start application integrated with Appwrite SDK.',
- 'score' => 6, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
+ 'score' => 9, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-tanstack-start-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-tanstack-start-light.png',
'frameworks' => [
@@ -1443,4 +1444,32 @@ return [
'providerVersion' => '0.3.*',
'variables' => []
],
+ [
+ 'key' => 'text-to-speech',
+ 'name' => 'Text-to-speech with ElevenLabs',
+ 'tagline' => 'Next.js app that transforms text into natural, human-like speech using ElevenLabs',
+ 'score' => 10, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
+ 'useCases' => [UseCases::AI],
+ 'screenshotDark' => $url . '/images/sites/templates/text-to-speech-dark.png',
+ 'screenshotLight' => $url . '/images/sites/templates/text-to-speech-light.png',
+ 'frameworks' => [
+ getFramework('NEXTJS', [
+ 'providerRootDirectory' => './nextjs/text-to-speech',
+ ]),
+ ],
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates-for-sites',
+ 'providerOwner' => 'appwrite',
+ 'providerVersion' => '0.6.*',
+ 'variables' => [
+ [
+ 'name' => 'ELEVENLABS_API_KEY',
+ 'description' => 'Your ElevenLabs API key',
+ 'value' => '',
+ 'placeholder' => 'sk_.....',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ ]
+ ],
];
diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php
index 9d1987591e..b7959bb6a9 100644
--- a/app/controllers/api/account.php
+++ b/app/controllers/api/account.php
@@ -20,7 +20,7 @@ use Appwrite\Event\Messaging;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
-use Appwrite\Network\Validator\Email;
+use Appwrite\Network\Validator\Email as EmailValidator;
use Appwrite\Network\Validator\Redirect;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\SDK\AuthType;
@@ -57,6 +57,7 @@ use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
+use Utopia\Emails\Email;
use Utopia\Locale\Locale;
use Utopia\Storage\Validator\FileName;
use Utopia\System\System;
@@ -337,7 +338,7 @@ App::post('/v1/account')
))
->label('abuse-limit', 10)
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
- ->param('email', '', new Email(), 'User email.')
+ ->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary'])
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('request')
@@ -394,6 +395,13 @@ App::post('/v1/account')
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
$password = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
+
+ try {
+ $emailCanonical = new Email($email);
+ } catch (Throwable) {
+ $emailCanonical = null;
+ }
+
try {
$userId = $userId == 'unique()' ? ID::unique() : $userId;
$user->setAttributes([
@@ -422,7 +430,13 @@ App::post('/v1/account')
'authenticators' => null,
'search' => implode(' ', [$userId, $email, $name]),
'accessedAt' => DateTime::now(),
+ 'emailCanonical' => $emailCanonical?->getCanonical(),
+ 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
+ 'emailIsCorporate' => $emailCanonical?->isCorporate(),
+ 'emailIsDisposable' => $emailCanonical?->isDisposable(),
+ 'emailIsFree' => $emailCanonical?->isFree(),
]);
+
$user->removeAttribute('$sequence');
$user = Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
try {
@@ -903,7 +917,7 @@ App::post('/v1/account/sessions/email')
))
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},email:{param-email}')
- ->param('email', '', new Email(), 'User email.')
+ ->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->inject('request')
->inject('response')
@@ -1598,6 +1612,12 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$failureRedirect(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
}
+ try {
+ $emailCanonical = new Email($email);
+ } catch (Throwable) {
+ $emailCanonical = null;
+ }
+
try {
$userId = ID::unique();
$user->setAttributes([
@@ -1625,7 +1645,13 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'authenticators' => null,
'search' => implode(' ', [$userId, $email, $name]),
'accessedAt' => DateTime::now(),
+ 'emailCanonical' => $emailCanonical?->getCanonical(),
+ 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
+ 'emailIsCorporate' => $emailCanonical?->isCorporate(),
+ 'emailIsDisposable' => $emailCanonical?->isDisposable(),
+ 'emailIsFree' => $emailCanonical?->isFree(),
]);
+
$user->removeAttribute('$sequence');
$userDoc = Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
$dbForProject->createDocument('targets', new Document([
@@ -1696,6 +1722,18 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
if (empty($user->getAttribute('email'))) {
$user->setAttribute('email', $oauth2->getUserEmail($accessToken));
+
+ try {
+ $emailCanonical = new Email($user->getAttribute('email'));
+ } catch (Throwable) {
+ $emailCanonical = null;
+ }
+
+ $user->setAttribute('emailCanonical', $emailCanonical?->getCanonical());
+ $user->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported());
+ $user->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate());
+ $user->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable());
+ $user->setAttribute('emailIsFree', $emailCanonical?->isFree());
}
if (empty($user->getAttribute('name'))) {
@@ -1944,7 +1982,7 @@ App::post('/v1/account/tokens/magic-url')
->label('abuse-limit', 60)
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
->param('userId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars. If the email address has never been used, a new account is created using the provided userId. Otherwise, if the email address is already attached to an account, the user ID is ignored.')
- ->param('email', '', new Email(), 'User email.')
+ ->param('email', '', new EmailValidator(), 'User email.')
->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey'])
->param('phrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.', true)
->inject('request')
@@ -1990,6 +2028,12 @@ App::post('/v1/account/tokens/magic-url')
$userId = $userId === 'unique()' ? ID::unique() : $userId;
+ try {
+ $emailCanonical = new Email($email);
+ } catch (Throwable) {
+ $emailCanonical = null;
+ }
+
$user->setAttributes([
'$id' => $userId,
'$permissions' => [
@@ -2014,6 +2058,11 @@ App::post('/v1/account/tokens/magic-url')
'authenticators' => null,
'search' => implode(' ', [$userId, $email]),
'accessedAt' => DateTime::now(),
+ 'emailCanonical' => $emailCanonical?->getCanonical(),
+ 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
+ 'emailIsCorporate' => $emailCanonical?->isCorporate(),
+ 'emailIsDisposable' => $emailCanonical?->isDisposable(),
+ 'emailIsFree' => $emailCanonical?->isFree(),
]);
$user->removeAttribute('$sequence');
@@ -2197,7 +2246,7 @@ App::post('/v1/account/tokens/email')
->label('abuse-limit', 10)
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars. If the email address has never been used, a new account is created using the provided userId. Otherwise, if the email address is already attached to an account, the user ID is ignored.')
- ->param('email', '', new Email(), 'User email.')
+ ->param('email', '', new EmailValidator(), 'User email.')
->param('phrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.', true)
->inject('request')
->inject('response')
@@ -2240,6 +2289,12 @@ App::post('/v1/account/tokens/email')
$userId = $userId === 'unique()' ? ID::unique() : $userId;
+ try {
+ $emailCanonical = new Email($email);
+ } catch (Throwable) {
+ $emailCanonical = null;
+ }
+
$user->setAttributes([
'$id' => $userId,
'$permissions' => [
@@ -2262,6 +2317,11 @@ App::post('/v1/account/tokens/email')
'memberships' => null,
'search' => implode(' ', [$userId, $email]),
'accessedAt' => DateTime::now(),
+ 'emailCanonical' => $emailCanonical?->getCanonical(),
+ 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
+ 'emailIsCorporate' => $emailCanonical?->isCorporate(),
+ 'emailIsDisposable' => $emailCanonical?->isDisposable(),
+ 'emailIsFree' => $emailCanonical?->isFree(),
]);
$user->removeAttribute('$sequence');
@@ -2609,6 +2669,11 @@ App::post('/v1/account/tokens/phone')
'memberships' => null,
'search' => implode(' ', [$userId, $phone]),
'accessedAt' => DateTime::now(),
+ 'emailCanonical' => null,
+ 'emailIsCanonical' => null,
+ 'emailIsCorporate' => null,
+ 'emailIsDisposable' => null,
+ 'emailIsFree' => null,
]);
$user->removeAttribute('$sequence');
@@ -3037,7 +3102,7 @@ App::patch('/v1/account/email')
],
contentType: ContentType::JSON
))
- ->param('email', '', new Email(), 'User email.')
+ ->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->inject('requestTimestamp')
->inject('response')
@@ -3072,9 +3137,20 @@ App::patch('/v1/account/email')
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
}
+ try {
+ $emailCanonical = new Email($email);
+ } catch (Throwable) {
+ $emailCanonical = null;
+ }
+
$user
->setAttribute('email', $email)
->setAttribute('emailVerification', false) // After this user needs to confirm mail again
+ ->setAttribute('emailCanonical', $emailCanonical?->getCanonical())
+ ->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported())
+ ->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate())
+ ->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable())
+ ->setAttribute('emailIsFree', $emailCanonical?->isFree())
;
if (empty($passwordUpdate)) {
@@ -3311,7 +3387,7 @@ App::post('/v1/account/recovery')
))
->label('abuse-limit', 10)
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
- ->param('email', '', new Email(), 'User email.')
+ ->param('email', '', new EmailValidator(), 'User email.')
->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['platforms', 'devKey'])
->inject('request')
->inject('response')
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index 9fb5db0c5b..554ef6f4fe 100644
--- a/app/controllers/api/teams.php
+++ b/app/controllers/api/teams.php
@@ -10,7 +10,7 @@ use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
-use Appwrite\Network\Validator\Email;
+use Appwrite\Network\Validator\Email as EmailValidator;
use Appwrite\Network\Validator\Redirect;
use Appwrite\Platform\Workers\Deletes;
use Appwrite\SDK\AuthType;
@@ -48,6 +48,7 @@ use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
+use Utopia\Emails\Email;
use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
@@ -468,7 +469,7 @@ App::post('/v1/teams/:teamId/memberships')
))
->label('abuse-limit', 10)
->param('teamId', '', new UID(), 'Team ID.')
- ->param('email', '', new Email(), 'Email of the new team member.', true)
+ ->param('email', '', new EmailValidator(), 'Email of the new team member.', true)
->param('userId', '', new UID(), 'ID of the user to be added to a team.', true)
->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('roles', [], function (Document $project) {
@@ -567,38 +568,52 @@ App::post('/v1/teams/:teamId/memberships')
}
try {
- $userId = ID::unique();
- $invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([
- '$id' => $userId,
- '$permissions' => [
- Permission::read(Role::any()),
- Permission::read(Role::user($userId)),
- Permission::update(Role::user($userId)),
- Permission::delete(Role::user($userId)),
- ],
- 'email' => empty($email) ? null : $email,
- 'phone' => empty($phone) ? null : $phone,
- 'emailVerification' => false,
- 'status' => true,
- // TODO: Set password empty?
- 'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
- 'hash' => Auth::DEFAULT_ALGO,
- 'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
- /**
- * Set the password update time to 0 for users created using
- * team invite and OAuth to allow password updates without an
- * old password
- */
- 'passwordUpdate' => null,
- 'registration' => DateTime::now(),
- 'reset' => false,
- 'name' => $name,
- 'prefs' => new \stdClass(),
- 'sessions' => null,
- 'tokens' => null,
- 'memberships' => null,
- 'search' => implode(' ', [$userId, $email, $name]),
- ])));
+ $emailCanonical = new Email($email);
+ } catch (Throwable) {
+ $emailCanonical = null;
+ }
+
+ $userId = ID::unique();
+
+ $userDocument = new Document([
+ '$id' => $userId,
+ '$permissions' => [
+ Permission::read(Role::any()),
+ Permission::read(Role::user($userId)),
+ Permission::update(Role::user($userId)),
+ Permission::delete(Role::user($userId)),
+ ],
+ 'email' => empty($email) ? null : $email,
+ 'phone' => empty($phone) ? null : $phone,
+ 'emailVerification' => false,
+ 'status' => true,
+ // TODO: Set password empty?
+ 'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
+ 'hash' => Auth::DEFAULT_ALGO,
+ 'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
+ /**
+ * Set the password update time to 0 for users created using
+ * team invite and OAuth to allow password updates without an
+ * old password
+ */
+ 'passwordUpdate' => null,
+ 'registration' => DateTime::now(),
+ 'reset' => false,
+ 'name' => $name,
+ 'prefs' => new \stdClass(),
+ 'sessions' => null,
+ 'tokens' => null,
+ 'memberships' => null,
+ 'search' => implode(' ', [$userId, $email, $name]),
+ 'emailCanonical' => $emailCanonical?->getCanonical(),
+ 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
+ 'emailIsCorporate' => $emailCanonical?->isCorporate(),
+ 'emailIsDisposable' => $emailCanonical?->isDisposable(),
+ 'emailIsFree' => $emailCanonical?->isFree(),
+ ]);
+
+ try {
+ $invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', $userDocument));
} catch (Duplicate $th) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php
index 582a3086a3..a8570c3079 100644
--- a/app/controllers/api/users.php
+++ b/app/controllers/api/users.php
@@ -16,7 +16,7 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
-use Appwrite\Network\Validator\Email;
+use Appwrite\Network\Validator\Email as EmailValidator;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
@@ -49,6 +49,7 @@ use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
+use Utopia\Emails\Email;
use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
@@ -98,6 +99,12 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
}
}
+ try {
+ $emailCanonical = new Email($email);
+ } catch (Throwable) {
+ $emailCanonical = null;
+ }
+
$password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null;
$user = new Document([
'$id' => $userId,
@@ -125,6 +132,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $phone, $name]),
+ 'emailCanonical' => $emailCanonical?->getCanonical(),
+ 'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
+ 'emailIsCorporate' => $emailCanonical?->isCorporate(),
+ 'emailIsDisposable' => $emailCanonical?->isDisposable(),
+ 'emailIsFree' => $emailCanonical?->isFree(),
]);
if ($hash === 'plaintext') {
@@ -209,7 +221,7 @@ App::post('/v1/users')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
- ->param('email', null, new Nullable(new Email()), 'User email.', true)
+ ->param('email', null, new Nullable(new EmailValidator()), 'User email.', true)
->param('phone', null, new Nullable(new Phone()), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'Plain text user password. Must be at least 8 chars.', true, ['project', 'passwordsDictionary'])
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
@@ -244,7 +256,7 @@ App::post('/v1/users/bcrypt')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
- ->param('email', '', new Email(), 'User email.')
+ ->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Bcrypt.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -279,7 +291,7 @@ App::post('/v1/users/md5')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
- ->param('email', '', new Email(), 'User email.')
+ ->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using MD5.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -314,7 +326,7 @@ App::post('/v1/users/argon2')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
- ->param('email', '', new Email(), 'User email.')
+ ->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Argon2.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -349,7 +361,7 @@ App::post('/v1/users/sha')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
- ->param('email', '', new Email(), 'User email.')
+ ->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using SHA.')
->param('passwordVersion', '', new WhiteList(['sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512']), "Optional SHA version used to hash password. Allowed values are: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512'", true)
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
@@ -391,7 +403,7 @@ App::post('/v1/users/phpass')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or pass the string `ID.unique()`to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
- ->param('email', '', new Email(), 'User email.')
+ ->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using PHPass.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -426,7 +438,7 @@ App::post('/v1/users/scrypt')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
- ->param('email', '', new Email(), 'User email.')
+ ->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Scrypt.')
->param('passwordSalt', '', new Text(128), 'Optional salt used to hash password.')
->param('passwordCpu', 8, new Integer(), 'Optional CPU cost used to hash password.')
@@ -474,7 +486,7 @@ App::post('/v1/users/scrypt-modified')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
- ->param('email', '', new Email(), 'User email.')
+ ->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Scrypt Modified.')
->param('passwordSalt', '', new Text(128), 'Salt used to hash password.')
->param('passwordSaltSeparator', '', new Text(128), 'Salt separator used to hash password.')
@@ -528,7 +540,7 @@ App::post('/v1/users/:userId/targets')
switch ($providerType) {
case 'email':
- $validator = new Email();
+ $validator = new EmailValidator();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
@@ -1403,7 +1415,7 @@ App::patch('/v1/users/:userId/email')
]
))
->param('userId', '', new UID(), 'User ID.')
- ->param('email', '', new Email(allowEmpty: true), 'User email.')
+ ->param('email', '', new EmailValidator(allowEmpty: true), 'User email.')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
@@ -1438,9 +1450,20 @@ App::patch('/v1/users/:userId/email')
$oldEmail = $user->getAttribute('email');
+ try {
+ $emailCanonical = new Email($email);
+ } catch (Throwable) {
+ $emailCanonical = null;
+ }
+
$user
->setAttribute('email', $email)
->setAttribute('emailVerification', false)
+ ->setAttribute('emailCanonical', $emailCanonical?->getCanonical())
+ ->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported())
+ ->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate())
+ ->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable())
+ ->setAttribute('emailIsFree', $emailCanonical?->isFree())
;
try {
@@ -1701,7 +1724,7 @@ App::patch('/v1/users/:userId/targets/:targetId')
switch ($providerType) {
case 'email':
- $validator = new Email();
+ $validator = new EmailValidator();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
diff --git a/app/controllers/general.php b/app/controllers/general.php
index 07de95a38f..e0435cd499 100644
--- a/app/controllers/general.php
+++ b/app/controllers/general.php
@@ -23,6 +23,7 @@ use Appwrite\Utopia\Request\Filters\V17 as RequestV17;
use Appwrite\Utopia\Request\Filters\V18 as RequestV18;
use Appwrite\Utopia\Request\Filters\V19 as RequestV19;
use Appwrite\Utopia\Request\Filters\V20 as RequestV20;
+use Appwrite\Utopia\Request\Filters\V21 as RequestV21;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
@@ -906,6 +907,9 @@ App::init()
$dbForProject = $getProjectDB($project);
$request->addFilter(new RequestV20($dbForProject, $route->getPathValues($request)));
}
+ if (version_compare($requestFormat, '1.9.0', '<')) {
+ $request->addFilter(new RequestV21());
+ }
}
$domain = $request->getHostname();
diff --git a/composer.lock b/composer.lock
index 87eaf28a3e..bf75e2935b 100644
--- a/composer.lock
+++ b/composer.lock
@@ -161,16 +161,16 @@
},
{
"name": "appwrite/php-runtimes",
- "version": "0.19.1",
+ "version": "0.19.2",
"source": {
"type": "git",
"url": "https://github.com/appwrite/runtimes.git",
- "reference": "7bd0cc3cb97de625d7b07230bd91b121f88e72ae"
+ "reference": "e5c142519df5aced37de9c302971c29c079ce3d9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/appwrite/runtimes/zipball/7bd0cc3cb97de625d7b07230bd91b121f88e72ae",
- "reference": "7bd0cc3cb97de625d7b07230bd91b121f88e72ae",
+ "url": "https://api.github.com/repos/appwrite/runtimes/zipball/e5c142519df5aced37de9c302971c29c079ce3d9",
+ "reference": "e5c142519df5aced37de9c302971c29c079ce3d9",
"shasum": ""
},
"require": {
@@ -210,9 +210,9 @@
],
"support": {
"issues": "https://github.com/appwrite/runtimes/issues",
- "source": "https://github.com/appwrite/runtimes/tree/0.19.1"
+ "source": "https://github.com/appwrite/runtimes/tree/0.19.2"
},
- "time": "2025-05-27T07:12:56+00:00"
+ "time": "2025-11-11T13:44:44+00:00"
},
{
"name": "beberlei/assert",
@@ -756,16 +756,16 @@
},
{
"name": "google/protobuf",
- "version": "v4.33.0",
+ "version": "v4.33.1",
"source": {
"type": "git",
"url": "https://github.com/protocolbuffers/protobuf-php.git",
- "reference": "b50269e23204e5ae859a326ec3d90f09efe3047d"
+ "reference": "0cd73ccf0cd26c3e72299cce1ea6144091a57e12"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/b50269e23204e5ae859a326ec3d90f09efe3047d",
- "reference": "b50269e23204e5ae859a326ec3d90f09efe3047d",
+ "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/0cd73ccf0cd26c3e72299cce1ea6144091a57e12",
+ "reference": "0cd73ccf0cd26c3e72299cce1ea6144091a57e12",
"shasum": ""
},
"require": {
@@ -794,9 +794,9 @@
"proto"
],
"support": {
- "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.0"
+ "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.1"
},
- "time": "2025-10-15T20:10:28+00:00"
+ "time": "2025-11-12T21:58:05+00:00"
},
{
"name": "league/csv",
@@ -2673,16 +2673,16 @@
},
{
"name": "symfony/http-client",
- "version": "v7.3.4",
+ "version": "v7.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
- "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62"
+ "reference": "3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-client/zipball/4b62871a01c49457cf2a8e560af7ee8a94b87a62",
- "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62",
+ "url": "https://api.github.com/repos/symfony/http-client/zipball/3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de",
+ "reference": "3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de",
"shasum": ""
},
"require": {
@@ -2749,7 +2749,7 @@
"http"
],
"support": {
- "source": "https://github.com/symfony/http-client/tree/v7.3.4"
+ "source": "https://github.com/symfony/http-client/tree/v7.3.6"
},
"funding": [
{
@@ -2769,7 +2769,7 @@
"type": "tidelift"
}
],
- "time": "2025-09-11T10:12:26+00:00"
+ "time": "2025-11-05T17:41:46+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -3176,16 +3176,16 @@
},
{
"name": "symfony/service-contracts",
- "version": "v3.6.0",
+ "version": "v3.6.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
- "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4"
+ "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
- "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43",
+ "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43",
"shasum": ""
},
"require": {
@@ -3239,7 +3239,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/service-contracts/tree/v3.6.0"
+ "source": "https://github.com/symfony/service-contracts/tree/v3.6.1"
},
"funding": [
{
@@ -3250,12 +3250,16 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-04-25T09:37:31+00:00"
+ "time": "2025-07-15T11:30:57+00:00"
},
{
"name": "tbachert/spi",
@@ -3840,16 +3844,16 @@
},
{
"name": "utopia-php/database",
- "version": "3.1.5",
+ "version": "3.4.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
- "reference": "76568b81f25d89fc1e0c53f0370f139130eeb939"
+ "reference": "e10b4faa4f3a3ef30a5f6d76acdb605469924aec"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/database/zipball/76568b81f25d89fc1e0c53f0370f139130eeb939",
- "reference": "76568b81f25d89fc1e0c53f0370f139130eeb939",
+ "url": "https://api.github.com/repos/utopia-php/database/zipball/e10b4faa4f3a3ef30a5f6d76acdb605469924aec",
+ "reference": "e10b4faa4f3a3ef30a5f6d76acdb605469924aec",
"shasum": ""
},
"require": {
@@ -3892,9 +3896,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
- "source": "https://github.com/utopia-php/database/tree/3.1.5"
+ "source": "https://github.com/utopia-php/database/tree/3.4.0"
},
- "time": "2025-11-05T10:17:55+00:00"
+ "time": "2025-11-13T06:34:20+00:00"
},
{
"name": "utopia-php/detector",
@@ -3943,22 +3947,24 @@
},
{
"name": "utopia-php/dns",
- "version": "1.1.0",
+ "version": "1.1.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/dns.git",
- "reference": "d6eca184883262bdcb4261e57491c91b16079b9a"
+ "reference": "1e6b4bac735329c9e5ec69a6a5d899ec2d050707"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/dns/zipball/d6eca184883262bdcb4261e57491c91b16079b9a",
- "reference": "d6eca184883262bdcb4261e57491c91b16079b9a",
+ "url": "https://api.github.com/repos/utopia-php/dns/zipball/1e6b4bac735329c9e5ec69a6a5d899ec2d050707",
+ "reference": "1e6b4bac735329c9e5ec69a6a5d899ec2d050707",
"shasum": ""
},
"require": {
"php": ">=8.3",
"utopia-php/console": "0.0.*",
- "utopia-php/telemetry": "0.1.*"
+ "utopia-php/domains": "0.9.*",
+ "utopia-php/telemetry": "0.1.*",
+ "utopia-php/validators": "^0.0.2"
},
"require-dev": {
"laravel/pint": "1.25.*",
@@ -3992,9 +3998,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/dns/issues",
- "source": "https://github.com/utopia-php/dns/tree/1.1.0"
+ "source": "https://github.com/utopia-php/dns/tree/1.1.3"
},
- "time": "2025-11-03T22:49:02+00:00"
+ "time": "2025-11-06T19:08:29+00:00"
},
{
"name": "utopia-php/domains",
@@ -5377,16 +5383,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
- "version": "1.5.1",
+ "version": "1.5.4",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
- "reference": "cd712674e34136f706e9170641ed6f4ce160e772"
+ "reference": "958947b6483a79e11c3812f23bb3056199fa4105"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/cd712674e34136f706e9170641ed6f4ce160e772",
- "reference": "cd712674e34136f706e9170641ed6f4ce160e772",
+ "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/958947b6483a79e11c3812f23bb3056199fa4105",
+ "reference": "958947b6483a79e11c3812f23bb3056199fa4105",
"shasum": ""
},
"require": {
@@ -5422,9 +5428,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
- "source": "https://github.com/appwrite/sdk-generator/tree/1.5.1"
+ "source": "https://github.com/appwrite/sdk-generator/tree/1.5.4"
},
- "time": "2025-11-04T09:55:47+00:00"
+ "time": "2025-11-12T12:43:42+00:00"
},
{
"name": "doctrine/annotations",
@@ -6077,24 +6083,24 @@
},
{
"name": "phpbench/container",
- "version": "2.2.2",
+ "version": "2.2.3",
"source": {
"type": "git",
"url": "https://github.com/phpbench/container.git",
- "reference": "a59b929e00b87b532ca6d0edd8eca0967655af33"
+ "reference": "0c7b2d36c1ea53fe27302fb8873ded7172047196"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpbench/container/zipball/a59b929e00b87b532ca6d0edd8eca0967655af33",
- "reference": "a59b929e00b87b532ca6d0edd8eca0967655af33",
+ "url": "https://api.github.com/repos/phpbench/container/zipball/0c7b2d36c1ea53fe27302fb8873ded7172047196",
+ "reference": "0c7b2d36c1ea53fe27302fb8873ded7172047196",
"shasum": ""
},
"require": {
"psr/container": "^1.0|^2.0",
- "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0 || ^7.0"
+ "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0 || ^7.0 || ^8.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^2.16",
+ "php-cs-fixer/shim": "^3.89",
"phpstan/phpstan": "^0.12.52",
"phpunit/phpunit": "^8"
},
@@ -6122,22 +6128,22 @@
"description": "Simple, configurable, service container.",
"support": {
"issues": "https://github.com/phpbench/container/issues",
- "source": "https://github.com/phpbench/container/tree/2.2.2"
+ "source": "https://github.com/phpbench/container/tree/2.2.3"
},
- "time": "2023-10-30T13:38:26+00:00"
+ "time": "2025-11-06T09:05:13+00:00"
},
{
"name": "phpbench/phpbench",
- "version": "1.4.2",
+ "version": "1.4.3",
"source": {
"type": "git",
"url": "https://github.com/phpbench/phpbench.git",
- "reference": "bb61ae6c54b3d58642be154eb09f4e73c3511018"
+ "reference": "b641dde59d969ea42eed70a39f9b51950bc96878"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpbench/phpbench/zipball/bb61ae6c54b3d58642be154eb09f4e73c3511018",
- "reference": "bb61ae6c54b3d58642be154eb09f4e73c3511018",
+ "url": "https://api.github.com/repos/phpbench/phpbench/zipball/b641dde59d969ea42eed70a39f9b51950bc96878",
+ "reference": "b641dde59d969ea42eed70a39f9b51950bc96878",
"shasum": ""
},
"require": {
@@ -6152,26 +6158,26 @@
"phpbench/container": "^2.2",
"psr/log": "^1.1 || ^2.0 || ^3.0",
"seld/jsonlint": "^1.1",
- "symfony/console": "^6.1 || ^7.0",
- "symfony/filesystem": "^6.1 || ^7.0",
- "symfony/finder": "^6.1 || ^7.0",
- "symfony/options-resolver": "^6.1 || ^7.0",
- "symfony/process": "^6.1 || ^7.0",
+ "symfony/console": "^6.1 || ^7.0 || ^8.0",
+ "symfony/filesystem": "^6.1 || ^7.0 || ^8.0",
+ "symfony/finder": "^6.1 || ^7.0 || ^8.0",
+ "symfony/options-resolver": "^6.1 || ^7.0 || ^8.0",
+ "symfony/process": "^6.1 || ^7.0 || ^8.0",
"webmozart/glob": "^4.6"
},
"require-dev": {
"dantleech/invoke": "^2.0",
"ergebnis/composer-normalize": "^2.39",
- "friendsofphp/php-cs-fixer": "^3.0",
"jangregor/phpstan-prophecy": "^1.0",
+ "php-cs-fixer/shim": "^3.9",
"phpspec/prophecy": "^1.22",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^10.4 || ^11.0",
"rector/rector": "^1.2",
- "symfony/error-handler": "^6.1 || ^7.0",
- "symfony/var-dumper": "^6.1 || ^7.0"
+ "symfony/error-handler": "^6.1 || ^7.0 || ^8.0",
+ "symfony/var-dumper": "^6.1 || ^7.0 || ^8.0"
},
"suggest": {
"ext-xdebug": "For Xdebug profiling extension."
@@ -6214,7 +6220,7 @@
],
"support": {
"issues": "https://github.com/phpbench/phpbench/issues",
- "source": "https://github.com/phpbench/phpbench/tree/1.4.2"
+ "source": "https://github.com/phpbench/phpbench/tree/1.4.3"
},
"funding": [
{
@@ -6222,7 +6228,7 @@
"type": "github"
}
],
- "time": "2025-10-26T14:21:59+00:00"
+ "time": "2025-11-06T19:07:31+00:00"
},
{
"name": "phpstan/phpstan",
@@ -7871,16 +7877,16 @@
},
{
"name": "symfony/console",
- "version": "v7.3.5",
+ "version": "v7.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7"
+ "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/cdb80fa5869653c83cfe1a9084a673b6daf57ea7",
- "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7",
+ "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a",
+ "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a",
"shasum": ""
},
"require": {
@@ -7945,7 +7951,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v7.3.5"
+ "source": "https://github.com/symfony/console/tree/v7.3.6"
},
"funding": [
{
@@ -7965,20 +7971,20 @@
"type": "tidelift"
}
],
- "time": "2025-10-14T15:46:26+00:00"
+ "time": "2025-11-04T01:21:42+00:00"
},
{
"name": "symfony/filesystem",
- "version": "v7.3.2",
+ "version": "v7.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd"
+ "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd",
- "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/e9bcfd7837928ab656276fe00464092cc9e1826a",
+ "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a",
"shasum": ""
},
"require": {
@@ -8015,7 +8021,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v7.3.2"
+ "source": "https://github.com/symfony/filesystem/tree/v7.3.6"
},
"funding": [
{
@@ -8035,7 +8041,7 @@
"type": "tidelift"
}
],
- "time": "2025-07-07T08:17:47+00:00"
+ "time": "2025-11-05T09:52:27+00:00"
},
{
"name": "symfony/finder",
diff --git a/docs/examples/1.8.x/client-android/java/avatars/get-screenshot.md b/docs/examples/1.8.x/client-android/java/avatars/get-screenshot.md
new file mode 100644
index 0000000000..077716f523
--- /dev/null
+++ b/docs/examples/1.8.x/client-android/java/avatars/get-screenshot.md
@@ -0,0 +1,41 @@
+import io.appwrite.Client;
+import io.appwrite.coroutines.CoroutineCallback;
+import io.appwrite.services.Avatars;
+
+Client client = new Client(context)
+ .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint
+ .setProject(""); // Your project ID
+
+Avatars avatars = new Avatars(client);
+
+avatars.getScreenshot(
+ "https://example.com", // url
+ mapOf( "a" to "b" ), // headers (optional)
+ 1, // viewportWidth (optional)
+ 1, // viewportHeight (optional)
+ 0.1, // scale (optional)
+ theme.LIGHT, // theme (optional)
+ "", // userAgent (optional)
+ false, // fullpage (optional)
+ "", // locale (optional)
+ timezone.AFRICA_ABIDJAN, // timezone (optional)
+ -90, // latitude (optional)
+ -180, // longitude (optional)
+ 0, // accuracy (optional)
+ false, // touch (optional)
+ listOf(), // permissions (optional)
+ 0, // sleep (optional)
+ 0, // width (optional)
+ 0, // height (optional)
+ -1, // quality (optional)
+ output.JPG, // output (optional)
+ new CoroutineCallback<>((result, error) -> {
+ if (error != null) {
+ error.printStackTrace();
+ return;
+ }
+
+ Log.d("Appwrite", result.toString());
+ })
+);
+
diff --git a/docs/examples/1.8.x/client-android/kotlin/avatars/get-screenshot.md b/docs/examples/1.8.x/client-android/kotlin/avatars/get-screenshot.md
new file mode 100644
index 0000000000..014ca90fd8
--- /dev/null
+++ b/docs/examples/1.8.x/client-android/kotlin/avatars/get-screenshot.md
@@ -0,0 +1,32 @@
+import io.appwrite.Client
+import io.appwrite.coroutines.CoroutineCallback
+import io.appwrite.services.Avatars
+
+val client = Client(context)
+ .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint
+ .setProject("") // Your project ID
+
+val avatars = Avatars(client)
+
+val result = avatars.getScreenshot(
+ url = "https://example.com",
+ headers = mapOf( "a" to "b" ), // (optional)
+ viewportWidth = 1, // (optional)
+ viewportHeight = 1, // (optional)
+ scale = 0.1, // (optional)
+ theme = theme.LIGHT, // (optional)
+ userAgent = "", // (optional)
+ fullpage = false, // (optional)
+ locale = "", // (optional)
+ timezone = timezone.AFRICA_ABIDJAN, // (optional)
+ latitude = -90, // (optional)
+ longitude = -180, // (optional)
+ accuracy = 0, // (optional)
+ touch = false, // (optional)
+ permissions = listOf(), // (optional)
+ sleep = 0, // (optional)
+ width = 0, // (optional)
+ height = 0, // (optional)
+ quality = -1, // (optional)
+ output = output.JPG, // (optional)
+)
\ No newline at end of file
diff --git a/docs/examples/1.8.x/client-apple/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/client-apple/examples/avatars/get-screenshot.md
new file mode 100644
index 0000000000..7f4ef5da5c
--- /dev/null
+++ b/docs/examples/1.8.x/client-apple/examples/avatars/get-screenshot.md
@@ -0,0 +1,32 @@
+import Appwrite
+import AppwriteEnums
+
+let client = Client()
+ .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint
+ .setProject("") // Your project ID
+
+let avatars = Avatars(client)
+
+let bytes = try await avatars.getScreenshot(
+ url: "https://example.com",
+ headers: [:], // optional
+ viewportWidth: 1, // optional
+ viewportHeight: 1, // optional
+ scale: 0.1, // optional
+ theme: .light, // optional
+ userAgent: "", // optional
+ fullpage: false, // optional
+ locale: "", // optional
+ timezone: .africaAbidjan, // optional
+ latitude: -90, // optional
+ longitude: -180, // optional
+ accuracy: 0, // optional
+ touch: false, // optional
+ permissions: [], // optional
+ sleep: 0, // optional
+ width: 0, // optional
+ height: 0, // optional
+ quality: -1, // optional
+ output: .jpg // optional
+)
+
diff --git a/docs/examples/1.8.x/client-flutter/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/client-flutter/examples/avatars/get-screenshot.md
new file mode 100644
index 0000000000..768cb8f271
--- /dev/null
+++ b/docs/examples/1.8.x/client-flutter/examples/avatars/get-screenshot.md
@@ -0,0 +1,65 @@
+import 'package:appwrite/appwrite.dart';
+
+Client client = Client()
+ .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
+ .setProject(''); // Your project ID
+
+Avatars avatars = Avatars(client);
+
+// Downloading file
+UInt8List bytes = await avatars.getScreenshot(
+ url: 'https://example.com',
+ headers: {}, // optional
+ viewportWidth: 1, // optional
+ viewportHeight: 1, // optional
+ scale: 0.1, // optional
+ theme: .light, // optional
+ userAgent: '', // optional
+ fullpage: false, // optional
+ locale: '', // optional
+ timezone: .africaAbidjan, // optional
+ latitude: -90, // optional
+ longitude: -180, // optional
+ accuracy: 0, // optional
+ touch: false, // optional
+ permissions: [], // optional
+ sleep: 0, // optional
+ width: 0, // optional
+ height: 0, // optional
+ quality: -1, // optional
+ output: .jpg, // optional
+)
+
+final file = File('path_to_file/filename.ext');
+file.writeAsBytesSync(bytes);
+
+// Displaying image preview
+FutureBuilder(
+ future: avatars.getScreenshot(
+ url:'https://example.com' ,
+ headers:{} , // optional
+ viewportWidth:1 , // optional
+ viewportHeight:1 , // optional
+ scale:0.1 , // optional
+ theme: .light, // optional
+ userAgent:'' , // optional
+ fullpage:false , // optional
+ locale:'' , // optional
+ timezone: .africaAbidjan, // optional
+ latitude:-90 , // optional
+ longitude:-180 , // optional
+ accuracy:0 , // optional
+ touch:false , // optional
+ permissions:[] , // optional
+ sleep:0 , // optional
+ width:0 , // optional
+ height:0 , // optional
+ quality:-1 , // optional
+ output: .jpg, // optional
+), // Works for both public file and private file, for private files you need to be logged in
+ builder: (context, snapshot) {
+ return snapshot.hasData && snapshot.data != null
+ ? Image.memory(snapshot.data)
+ : CircularProgressIndicator();
+ }
+);
diff --git a/docs/examples/1.8.x/client-graphql/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/client-graphql/examples/avatars/get-screenshot.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs/examples/1.8.x/client-react-native/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/client-react-native/examples/avatars/get-screenshot.md
new file mode 100644
index 0000000000..7482b4cf0e
--- /dev/null
+++ b/docs/examples/1.8.x/client-react-native/examples/avatars/get-screenshot.md
@@ -0,0 +1,32 @@
+import { Client, Avatars, , , } from "react-native-appwrite";
+
+const client = new Client()
+ .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
+ .setProject(''); // Your project ID
+
+const avatars = new Avatars(client);
+
+const result = avatars.getScreenshot({
+ url: 'https://example.com',
+ headers: {}, // optional
+ viewportWidth: 1, // optional
+ viewportHeight: 1, // optional
+ scale: 0.1, // optional
+ theme: .Light, // optional
+ userAgent: '', // optional
+ fullpage: false, // optional
+ locale: '', // optional
+ timezone: .AfricaAbidjan, // optional
+ latitude: -90, // optional
+ longitude: -180, // optional
+ accuracy: 0, // optional
+ touch: false, // optional
+ permissions: [], // optional
+ sleep: 0, // optional
+ width: 0, // optional
+ height: 0, // optional
+ quality: -1, // optional
+ output: .Jpg // optional
+});
+
+console.log(result);
diff --git a/docs/examples/1.8.x/client-rest/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/client-rest/examples/avatars/get-screenshot.md
new file mode 100644
index 0000000000..b4c31ca100
--- /dev/null
+++ b/docs/examples/1.8.x/client-rest/examples/avatars/get-screenshot.md
@@ -0,0 +1,6 @@
+GET /v1/avatars/screenshots HTTP/1.1
+Host: cloud.appwrite.io
+X-Appwrite-Response-Format: 1.8.0
+X-Appwrite-Project:
+X-Appwrite-Session:
+X-Appwrite-JWT:
diff --git a/docs/examples/1.8.x/client-web/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/client-web/examples/avatars/get-screenshot.md
new file mode 100644
index 0000000000..c4722be633
--- /dev/null
+++ b/docs/examples/1.8.x/client-web/examples/avatars/get-screenshot.md
@@ -0,0 +1,32 @@
+import { Client, Avatars, , , } from "appwrite";
+
+const client = new Client()
+ .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
+ .setProject(''); // Your project ID
+
+const avatars = new Avatars(client);
+
+const result = avatars.getScreenshot({
+ url: 'https://example.com',
+ headers: {}, // optional
+ viewportWidth: 1, // optional
+ viewportHeight: 1, // optional
+ scale: 0.1, // optional
+ theme: .Light, // optional
+ userAgent: '', // optional
+ fullpage: false, // optional
+ locale: '', // optional
+ timezone: .AfricaAbidjan, // optional
+ latitude: -90, // optional
+ longitude: -180, // optional
+ accuracy: 0, // optional
+ touch: false, // optional
+ permissions: [], // optional
+ sleep: 0, // optional
+ width: 0, // optional
+ height: 0, // optional
+ quality: -1, // optional
+ output: .Jpg // optional
+});
+
+console.log(result);
diff --git a/docs/examples/1.8.x/console-web/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/console-web/examples/avatars/get-screenshot.md
new file mode 100644
index 0000000000..3a9437515d
--- /dev/null
+++ b/docs/examples/1.8.x/console-web/examples/avatars/get-screenshot.md
@@ -0,0 +1,32 @@
+import { Client, Avatars, , , } from "@appwrite.io/console";
+
+const client = new Client()
+ .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
+ .setProject(''); // Your project ID
+
+const avatars = new Avatars(client);
+
+const result = avatars.getScreenshot({
+ url: 'https://example.com',
+ headers: {}, // optional
+ viewportWidth: 1, // optional
+ viewportHeight: 1, // optional
+ scale: 0.1, // optional
+ theme: .Light, // optional
+ userAgent: '', // optional
+ fullpage: false, // optional
+ locale: '', // optional
+ timezone: .AfricaAbidjan, // optional
+ latitude: -90, // optional
+ longitude: -180, // optional
+ accuracy: 0, // optional
+ touch: false, // optional
+ permissions: [], // optional
+ sleep: 0, // optional
+ width: 0, // optional
+ height: 0, // optional
+ quality: -1, // optional
+ output: .Jpg // optional
+});
+
+console.log(result);
diff --git a/docs/examples/1.8.x/server-dart/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-dart/examples/avatars/get-screenshot.md
new file mode 100644
index 0000000000..7630648f98
--- /dev/null
+++ b/docs/examples/1.8.x/server-dart/examples/avatars/get-screenshot.md
@@ -0,0 +1,31 @@
+import 'package:dart_appwrite/dart_appwrite.dart';
+
+Client client = Client()
+ .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
+ .setProject('') // Your project ID
+ .setSession(''); // The user session to authenticate with
+
+Avatars avatars = Avatars(client);
+
+UInt8List result = await avatars.getScreenshot(
+ url: 'https://example.com',
+ headers: {}, // (optional)
+ viewportWidth: 1, // (optional)
+ viewportHeight: 1, // (optional)
+ scale: 0.1, // (optional)
+ theme: .light, // (optional)
+ userAgent: '', // (optional)
+ fullpage: false, // (optional)
+ locale: '', // (optional)
+ timezone: .africaAbidjan, // (optional)
+ latitude: -90, // (optional)
+ longitude: -180, // (optional)
+ accuracy: 0, // (optional)
+ touch: false, // (optional)
+ permissions: [], // (optional)
+ sleep: 0, // (optional)
+ width: 0, // (optional)
+ height: 0, // (optional)
+ quality: -1, // (optional)
+ output: .jpg, // (optional)
+);
diff --git a/docs/examples/1.8.x/server-dotnet/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-dotnet/examples/avatars/get-screenshot.md
new file mode 100644
index 0000000000..f5c3542a97
--- /dev/null
+++ b/docs/examples/1.8.x/server-dotnet/examples/avatars/get-screenshot.md
@@ -0,0 +1,34 @@
+using Appwrite;
+using Appwrite.Enums;
+using Appwrite.Models;
+using Appwrite.Services;
+
+Client client = new Client()
+ .SetEndPoint("https://.cloud.appwrite.io/v1") // Your API Endpoint
+ .SetProject("") // Your project ID
+ .SetSession(""); // The user session to authenticate with
+
+Avatars avatars = new Avatars(client);
+
+byte[] result = await avatars.GetScreenshot(
+ url: "https://example.com",
+ headers: [object], // optional
+ viewportWidth: 1, // optional
+ viewportHeight: 1, // optional
+ scale: 0.1, // optional
+ theme: .Light, // optional
+ userAgent: "", // optional
+ fullpage: false, // optional
+ locale: "", // optional
+ timezone: .AfricaAbidjan, // optional
+ latitude: -90, // optional
+ longitude: -180, // optional
+ accuracy: 0, // optional
+ touch: false, // optional
+ permissions: new List(), // optional
+ sleep: 0, // optional
+ width: 0, // optional
+ height: 0, // optional
+ quality: -1, // optional
+ output: .Jpg // optional
+);
\ No newline at end of file
diff --git a/docs/examples/1.8.x/server-go/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-go/examples/avatars/get-screenshot.md
new file mode 100644
index 0000000000..ac425fbc4f
--- /dev/null
+++ b/docs/examples/1.8.x/server-go/examples/avatars/get-screenshot.md
@@ -0,0 +1,38 @@
+package main
+
+import (
+ "fmt"
+ "github.com/appwrite/sdk-for-go/client"
+ "github.com/appwrite/sdk-for-go/avatars"
+)
+
+client := client.New(
+ client.WithEndpoint("https://.cloud.appwrite.io/v1")
+ client.WithProject("")
+ client.WithSession("")
+)
+
+service := avatars.New(client)
+
+response, error := service.GetScreenshot(
+ "https://example.com",
+ avatars.WithGetScreenshotHeaders(map[string]interface{}{}),
+ avatars.WithGetScreenshotViewportWidth(1),
+ avatars.WithGetScreenshotViewportHeight(1),
+ avatars.WithGetScreenshotScale(0.1),
+ avatars.WithGetScreenshotTheme("light"),
+ avatars.WithGetScreenshotUserAgent(""),
+ avatars.WithGetScreenshotFullpage(false),
+ avatars.WithGetScreenshotLocale(""),
+ avatars.WithGetScreenshotTimezone("africa/abidjan"),
+ avatars.WithGetScreenshotLatitude(-90),
+ avatars.WithGetScreenshotLongitude(-180),
+ avatars.WithGetScreenshotAccuracy(0),
+ avatars.WithGetScreenshotTouch(false),
+ avatars.WithGetScreenshotPermissions([]interface{}{}),
+ avatars.WithGetScreenshotSleep(0),
+ avatars.WithGetScreenshotWidth(0),
+ avatars.WithGetScreenshotHeight(0),
+ avatars.WithGetScreenshotQuality(-1),
+ avatars.WithGetScreenshotOutput("jpg"),
+)
diff --git a/docs/examples/1.8.x/server-graphql/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-graphql/examples/avatars/get-screenshot.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs/examples/1.8.x/server-kotlin/java/avatars/get-screenshot.md b/docs/examples/1.8.x/server-kotlin/java/avatars/get-screenshot.md
new file mode 100644
index 0000000000..cf734af3b2
--- /dev/null
+++ b/docs/examples/1.8.x/server-kotlin/java/avatars/get-screenshot.md
@@ -0,0 +1,42 @@
+import io.appwrite.Client;
+import io.appwrite.coroutines.CoroutineCallback;
+import io.appwrite.services.Avatars;
+
+Client client = new Client()
+ .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint
+ .setProject("") // Your project ID
+ .setSession(""); // The user session to authenticate with
+
+Avatars avatars = new Avatars(client);
+
+avatars.getScreenshot(
+ "https://example.com", // url
+ mapOf( "a" to "b" ), // headers (optional)
+ 1, // viewportWidth (optional)
+ 1, // viewportHeight (optional)
+ 0.1, // scale (optional)
+ .LIGHT, // theme (optional)
+ "", // userAgent (optional)
+ false, // fullpage (optional)
+ "", // locale (optional)
+ .AFRICA_ABIDJAN, // timezone (optional)
+ -90, // latitude (optional)
+ -180, // longitude (optional)
+ 0, // accuracy (optional)
+ false, // touch (optional)
+ listOf(), // permissions (optional)
+ 0, // sleep (optional)
+ 0, // width (optional)
+ 0, // height (optional)
+ -1, // quality (optional)
+ .JPG, // output (optional)
+ new CoroutineCallback<>((result, error) -> {
+ if (error != null) {
+ error.printStackTrace();
+ return;
+ }
+
+ System.out.println(result);
+ })
+);
+
diff --git a/docs/examples/1.8.x/server-kotlin/kotlin/avatars/get-screenshot.md b/docs/examples/1.8.x/server-kotlin/kotlin/avatars/get-screenshot.md
new file mode 100644
index 0000000000..96032bb8a4
--- /dev/null
+++ b/docs/examples/1.8.x/server-kotlin/kotlin/avatars/get-screenshot.md
@@ -0,0 +1,33 @@
+import io.appwrite.Client
+import io.appwrite.coroutines.CoroutineCallback
+import io.appwrite.services.Avatars
+
+val client = Client()
+ .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint
+ .setProject("") // Your project ID
+ .setSession("") // The user session to authenticate with
+
+val avatars = Avatars(client)
+
+val result = avatars.getScreenshot(
+ url = "https://example.com",
+ headers = mapOf( "a" to "b" ), // optional
+ viewportWidth = 1, // optional
+ viewportHeight = 1, // optional
+ scale = 0.1, // optional
+ theme = "light", // optional
+ userAgent = "", // optional
+ fullpage = false, // optional
+ locale = "", // optional
+ timezone = "africa/abidjan", // optional
+ latitude = -90, // optional
+ longitude = -180, // optional
+ accuracy = 0, // optional
+ touch = false, // optional
+ permissions = listOf(), // optional
+ sleep = 0, // optional
+ width = 0, // optional
+ height = 0, // optional
+ quality = -1, // optional
+ output = "jpg" // optional
+)
diff --git a/docs/examples/1.8.x/server-nodejs/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-nodejs/examples/avatars/get-screenshot.md
new file mode 100644
index 0000000000..5f7b40cece
--- /dev/null
+++ b/docs/examples/1.8.x/server-nodejs/examples/avatars/get-screenshot.md
@@ -0,0 +1,31 @@
+const sdk = require('node-appwrite');
+
+const client = new sdk.Client()
+ .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
+ .setProject('') // Your project ID
+ .setSession(''); // The user session to authenticate with
+
+const avatars = new sdk.Avatars(client);
+
+const result = await avatars.getScreenshot({
+ url: 'https://example.com',
+ headers: {}, // optional
+ viewportWidth: 1, // optional
+ viewportHeight: 1, // optional
+ scale: 0.1, // optional
+ theme: sdk..Light, // optional
+ userAgent: '', // optional
+ fullpage: false, // optional
+ locale: '', // optional
+ timezone: sdk..AfricaAbidjan, // optional
+ latitude: -90, // optional
+ longitude: -180, // optional
+ accuracy: 0, // optional
+ touch: false, // optional
+ permissions: [], // optional
+ sleep: 0, // optional
+ width: 0, // optional
+ height: 0, // optional
+ quality: -1, // optional
+ output: sdk..Jpg // optional
+});
diff --git a/docs/examples/1.8.x/server-php/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-php/examples/avatars/get-screenshot.md
new file mode 100644
index 0000000000..b9dfd23862
--- /dev/null
+++ b/docs/examples/1.8.x/server-php/examples/avatars/get-screenshot.md
@@ -0,0 +1,37 @@
+setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
+ ->setProject('') // Your project ID
+ ->setSession(''); // The user session to authenticate with
+
+$avatars = new Avatars($client);
+
+$result = $avatars->getScreenshot(
+ url: 'https://example.com',
+ headers: [], // optional
+ viewportWidth: 1, // optional
+ viewportHeight: 1, // optional
+ scale: 0.1, // optional
+ theme: Theme::LIGHT(), // optional
+ userAgent: '', // optional
+ fullpage: false, // optional
+ locale: '', // optional
+ timezone: Timezone::AFRICAABIDJAN(), // optional
+ latitude: -90, // optional
+ longitude: -180, // optional
+ accuracy: 0, // optional
+ touch: false, // optional
+ permissions: [], // optional
+ sleep: 0, // optional
+ width: 0, // optional
+ height: 0, // optional
+ quality: -1, // optional
+ output: Output::JPG() // optional
+);
\ No newline at end of file
diff --git a/docs/examples/1.8.x/server-php/examples/databases/create-relationship-attribute.md b/docs/examples/1.8.x/server-php/examples/databases/create-relationship-attribute.md
index caccd36031..551fe17a9d 100644
--- a/docs/examples/1.8.x/server-php/examples/databases/create-relationship-attribute.md
+++ b/docs/examples/1.8.x/server-php/examples/databases/create-relationship-attribute.md
@@ -3,6 +3,7 @@
use Appwrite\Client;
use Appwrite\Services\Databases;
use Appwrite\Enums\RelationshipType;
+use Appwrite\Enums\RelationMutate;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
diff --git a/docs/examples/1.8.x/server-php/examples/databases/update-relationship-attribute.md b/docs/examples/1.8.x/server-php/examples/databases/update-relationship-attribute.md
index 01783cf3bf..a4d6888711 100644
--- a/docs/examples/1.8.x/server-php/examples/databases/update-relationship-attribute.md
+++ b/docs/examples/1.8.x/server-php/examples/databases/update-relationship-attribute.md
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Databases;
+use Appwrite\Enums\RelationMutate;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
diff --git a/docs/examples/1.8.x/server-php/examples/functions/create-execution.md b/docs/examples/1.8.x/server-php/examples/functions/create-execution.md
index cd11b5ea6e..9c12e87374 100644
--- a/docs/examples/1.8.x/server-php/examples/functions/create-execution.md
+++ b/docs/examples/1.8.x/server-php/examples/functions/create-execution.md
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Functions;
+use Appwrite\Enums\ExecutionMethod;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
diff --git a/docs/examples/1.8.x/server-php/examples/functions/create.md b/docs/examples/1.8.x/server-php/examples/functions/create.md
index 3d37b8068e..f7176871bd 100644
--- a/docs/examples/1.8.x/server-php/examples/functions/create.md
+++ b/docs/examples/1.8.x/server-php/examples/functions/create.md
@@ -2,7 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Functions;
-use Appwrite\Enums\;
+use Appwrite\Enums\Runtime;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
@@ -14,7 +14,7 @@ $functions = new Functions($client);
$result = $functions->create(
functionId: '',
name: '',
- runtime: ::NODE145(),
+ runtime: Runtime::NODE145(),
execute: ["any"], // optional
events: [], // optional
schedule: '', // optional
diff --git a/docs/examples/1.8.x/server-php/examples/functions/get-deployment-download.md b/docs/examples/1.8.x/server-php/examples/functions/get-deployment-download.md
index 7b3e18751e..a06f97b662 100644
--- a/docs/examples/1.8.x/server-php/examples/functions/get-deployment-download.md
+++ b/docs/examples/1.8.x/server-php/examples/functions/get-deployment-download.md
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Functions;
+use Appwrite\Enums\DeploymentDownloadType;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
diff --git a/docs/examples/1.8.x/server-php/examples/functions/update.md b/docs/examples/1.8.x/server-php/examples/functions/update.md
index ea8d863ae5..da5ee88931 100644
--- a/docs/examples/1.8.x/server-php/examples/functions/update.md
+++ b/docs/examples/1.8.x/server-php/examples/functions/update.md
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Functions;
+use Appwrite\Enums\Runtime;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
@@ -13,7 +14,7 @@ $functions = new Functions($client);
$result = $functions->update(
functionId: '',
name: '',
- runtime: ::NODE145(), // optional
+ runtime: Runtime::NODE145(), // optional
execute: ["any"], // optional
events: [], // optional
schedule: '', // optional
diff --git a/docs/examples/1.8.x/server-php/examples/health/get-failed-jobs.md b/docs/examples/1.8.x/server-php/examples/health/get-failed-jobs.md
index 02959db3b5..63bc1c83f2 100644
--- a/docs/examples/1.8.x/server-php/examples/health/get-failed-jobs.md
+++ b/docs/examples/1.8.x/server-php/examples/health/get-failed-jobs.md
@@ -2,7 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Health;
-use Appwrite\Enums\;
+use Appwrite\Enums\Name;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
@@ -12,6 +12,6 @@ $client = (new Client())
$health = new Health($client);
$result = $health->getFailedJobs(
- name: ::V1DATABASE(),
+ name: Name::V1DATABASE(),
threshold: null // optional
);
\ No newline at end of file
diff --git a/docs/examples/1.8.x/server-php/examples/messaging/create-push.md b/docs/examples/1.8.x/server-php/examples/messaging/create-push.md
index 51fc0d0a92..614c758c80 100644
--- a/docs/examples/1.8.x/server-php/examples/messaging/create-push.md
+++ b/docs/examples/1.8.x/server-php/examples/messaging/create-push.md
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Messaging;
+use Appwrite\Enums\MessagePriority;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
diff --git a/docs/examples/1.8.x/server-php/examples/messaging/create-smtp-provider.md b/docs/examples/1.8.x/server-php/examples/messaging/create-smtp-provider.md
index 017f20cc15..953bbcf44f 100644
--- a/docs/examples/1.8.x/server-php/examples/messaging/create-smtp-provider.md
+++ b/docs/examples/1.8.x/server-php/examples/messaging/create-smtp-provider.md
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Messaging;
+use Appwrite\Enums\SmtpEncryption;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
diff --git a/docs/examples/1.8.x/server-php/examples/messaging/update-push.md b/docs/examples/1.8.x/server-php/examples/messaging/update-push.md
index 05a51783c9..0fea9a135f 100644
--- a/docs/examples/1.8.x/server-php/examples/messaging/update-push.md
+++ b/docs/examples/1.8.x/server-php/examples/messaging/update-push.md
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Messaging;
+use Appwrite\Enums\MessagePriority;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
diff --git a/docs/examples/1.8.x/server-php/examples/messaging/update-smtp-provider.md b/docs/examples/1.8.x/server-php/examples/messaging/update-smtp-provider.md
index 3bc80d2789..495f332131 100644
--- a/docs/examples/1.8.x/server-php/examples/messaging/update-smtp-provider.md
+++ b/docs/examples/1.8.x/server-php/examples/messaging/update-smtp-provider.md
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Messaging;
+use Appwrite\Enums\SmtpEncryption;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
diff --git a/docs/examples/1.8.x/server-php/examples/sites/create.md b/docs/examples/1.8.x/server-php/examples/sites/create.md
index 4a1c3a4fcb..6f1fc5ac27 100644
--- a/docs/examples/1.8.x/server-php/examples/sites/create.md
+++ b/docs/examples/1.8.x/server-php/examples/sites/create.md
@@ -2,8 +2,9 @@
use Appwrite\Client;
use Appwrite\Services\Sites;
-use Appwrite\Enums\;
-use Appwrite\Enums\;
+use Appwrite\Enums\Framework;
+use Appwrite\Enums\BuildRuntime;
+use Appwrite\Enums\Adapter;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
@@ -15,15 +16,15 @@ $sites = new Sites($client);
$result = $sites->create(
siteId: '',
name: '',
- framework: ::ANALOG(),
- buildRuntime: ::NODE145(),
+ framework: Framework::ANALOG(),
+ buildRuntime: BuildRuntime::NODE145(),
enabled: false, // optional
logging: false, // optional
timeout: 1, // optional
installCommand: '', // optional
buildCommand: '', // optional
outputDirectory: '', // optional
- adapter: ::STATIC(), // optional
+ adapter: Adapter::STATIC(), // optional
installationId: '', // optional
fallbackFile: '', // optional
providerRepositoryId: '', // optional
diff --git a/docs/examples/1.8.x/server-php/examples/sites/get-deployment-download.md b/docs/examples/1.8.x/server-php/examples/sites/get-deployment-download.md
index 91c6b6e52a..61fad0bd74 100644
--- a/docs/examples/1.8.x/server-php/examples/sites/get-deployment-download.md
+++ b/docs/examples/1.8.x/server-php/examples/sites/get-deployment-download.md
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Sites;
+use Appwrite\Enums\DeploymentDownloadType;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
diff --git a/docs/examples/1.8.x/server-php/examples/sites/update.md b/docs/examples/1.8.x/server-php/examples/sites/update.md
index f2ca54a987..d2a6c9d256 100644
--- a/docs/examples/1.8.x/server-php/examples/sites/update.md
+++ b/docs/examples/1.8.x/server-php/examples/sites/update.md
@@ -2,7 +2,9 @@
use Appwrite\Client;
use Appwrite\Services\Sites;
-use Appwrite\Enums\;
+use Appwrite\Enums\Framework;
+use Appwrite\Enums\BuildRuntime;
+use Appwrite\Enums\Adapter;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
@@ -14,15 +16,15 @@ $sites = new Sites($client);
$result = $sites->update(
siteId: '',
name: '',
- framework: ::ANALOG(),
+ framework: Framework::ANALOG(),
enabled: false, // optional
logging: false, // optional
timeout: 1, // optional
installCommand: '', // optional
buildCommand: '', // optional
outputDirectory: '', // optional
- buildRuntime: ::NODE145(), // optional
- adapter: ::STATIC(), // optional
+ buildRuntime: BuildRuntime::NODE145(), // optional
+ adapter: Adapter::STATIC(), // optional
fallbackFile: '', // optional
installationId: '', // optional
providerRepositoryId: '', // optional
diff --git a/docs/examples/1.8.x/server-php/examples/storage/create-bucket.md b/docs/examples/1.8.x/server-php/examples/storage/create-bucket.md
index 2e7cc1d15c..3d4f717e4d 100644
--- a/docs/examples/1.8.x/server-php/examples/storage/create-bucket.md
+++ b/docs/examples/1.8.x/server-php/examples/storage/create-bucket.md
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Storage;
+use Appwrite\Enums\Compression;
use Appwrite\Permission;
use Appwrite\Role;
@@ -20,7 +21,7 @@ $result = $storage->createBucket(
enabled: false, // optional
maximumFileSize: 1, // optional
allowedFileExtensions: [], // optional
- compression: ::NONE(), // optional
+ compression: Compression::NONE(), // optional
encryption: false, // optional
antivirus: false // optional
);
\ No newline at end of file
diff --git a/docs/examples/1.8.x/server-php/examples/storage/get-file-preview.md b/docs/examples/1.8.x/server-php/examples/storage/get-file-preview.md
index 0b65fc326a..aaa15a22fb 100644
--- a/docs/examples/1.8.x/server-php/examples/storage/get-file-preview.md
+++ b/docs/examples/1.8.x/server-php/examples/storage/get-file-preview.md
@@ -2,6 +2,8 @@
use Appwrite\Client;
use Appwrite\Services\Storage;
+use Appwrite\Enums\ImageGravity;
+use Appwrite\Enums\ImageFormat;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
diff --git a/docs/examples/1.8.x/server-php/examples/storage/update-bucket.md b/docs/examples/1.8.x/server-php/examples/storage/update-bucket.md
index 819798cb95..77f4262c2d 100644
--- a/docs/examples/1.8.x/server-php/examples/storage/update-bucket.md
+++ b/docs/examples/1.8.x/server-php/examples/storage/update-bucket.md
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Storage;
+use Appwrite\Enums\Compression;
use Appwrite\Permission;
use Appwrite\Role;
@@ -20,7 +21,7 @@ $result = $storage->updateBucket(
enabled: false, // optional
maximumFileSize: 1, // optional
allowedFileExtensions: [], // optional
- compression: ::NONE(), // optional
+ compression: Compression::NONE(), // optional
encryption: false, // optional
antivirus: false // optional
);
\ No newline at end of file
diff --git a/docs/examples/1.8.x/server-php/examples/tablesdb/create-relationship-column.md b/docs/examples/1.8.x/server-php/examples/tablesdb/create-relationship-column.md
index 031d1fd1aa..7f9a06cc03 100644
--- a/docs/examples/1.8.x/server-php/examples/tablesdb/create-relationship-column.md
+++ b/docs/examples/1.8.x/server-php/examples/tablesdb/create-relationship-column.md
@@ -3,6 +3,7 @@
use Appwrite\Client;
use Appwrite\Services\TablesDB;
use Appwrite\Enums\RelationshipType;
+use Appwrite\Enums\RelationMutate;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
diff --git a/docs/examples/1.8.x/server-php/examples/tablesdb/update-relationship-column.md b/docs/examples/1.8.x/server-php/examples/tablesdb/update-relationship-column.md
index 834dc18cee..cc2e2ccaef 100644
--- a/docs/examples/1.8.x/server-php/examples/tablesdb/update-relationship-column.md
+++ b/docs/examples/1.8.x/server-php/examples/tablesdb/update-relationship-column.md
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\TablesDB;
+use Appwrite\Enums\RelationMutate;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
diff --git a/docs/examples/1.8.x/server-php/examples/users/create-sha-user.md b/docs/examples/1.8.x/server-php/examples/users/create-sha-user.md
index 0b9a27ed8e..812bcd5eb5 100644
--- a/docs/examples/1.8.x/server-php/examples/users/create-sha-user.md
+++ b/docs/examples/1.8.x/server-php/examples/users/create-sha-user.md
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Users;
+use Appwrite\Enums\PasswordHash;
$client = (new Client())
->setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint
diff --git a/docs/examples/1.8.x/server-python/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-python/examples/avatars/get-screenshot.md
new file mode 100644
index 0000000000..34bdf8ac7a
--- /dev/null
+++ b/docs/examples/1.8.x/server-python/examples/avatars/get-screenshot.md
@@ -0,0 +1,32 @@
+from appwrite.client import Client
+from appwrite.services.avatars import Avatars
+
+client = Client()
+client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint
+client.set_project('') # Your project ID
+client.set_session('') # The user session to authenticate with
+
+avatars = Avatars(client)
+
+result = avatars.get_screenshot(
+ url = 'https://example.com',
+ headers = {}, # optional
+ viewport_width = 1, # optional
+ viewport_height = 1, # optional
+ scale = 0.1, # optional
+ theme = .LIGHT, # optional
+ user_agent = '', # optional
+ fullpage = False, # optional
+ locale = '', # optional
+ timezone = .AFRICA_ABIDJAN, # optional
+ latitude = -90, # optional
+ longitude = -180, # optional
+ accuracy = 0, # optional
+ touch = False, # optional
+ permissions = [], # optional
+ sleep = 0, # optional
+ width = 0, # optional
+ height = 0, # optional
+ quality = -1, # optional
+ output = .JPG # optional
+)
diff --git a/docs/examples/1.8.x/server-rest/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-rest/examples/avatars/get-screenshot.md
new file mode 100644
index 0000000000..0ab16b59e6
--- /dev/null
+++ b/docs/examples/1.8.x/server-rest/examples/avatars/get-screenshot.md
@@ -0,0 +1,7 @@
+GET /v1/avatars/screenshots HTTP/1.1
+Host: cloud.appwrite.io
+X-Appwrite-Response-Format: 1.8.0
+X-Appwrite-Project:
+X-Appwrite-Session:
+X-Appwrite-Key:
+X-Appwrite-JWT:
diff --git a/docs/examples/1.8.x/server-ruby/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-ruby/examples/avatars/get-screenshot.md
new file mode 100644
index 0000000000..f2af537fe8
--- /dev/null
+++ b/docs/examples/1.8.x/server-ruby/examples/avatars/get-screenshot.md
@@ -0,0 +1,33 @@
+require 'appwrite'
+
+include Appwrite
+
+client = Client.new
+ .set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint
+ .set_project('') # Your project ID
+ .set_session('') # The user session to authenticate with
+
+avatars = Avatars.new(client)
+
+result = avatars.get_screenshot(
+ url: 'https://example.com',
+ headers: {}, # optional
+ viewport_width: 1, # optional
+ viewport_height: 1, # optional
+ scale: 0.1, # optional
+ theme: ::LIGHT, # optional
+ user_agent: '', # optional
+ fullpage: false, # optional
+ locale: '', # optional
+ timezone: ::AFRICA_ABIDJAN, # optional
+ latitude: -90, # optional
+ longitude: -180, # optional
+ accuracy: 0, # optional
+ touch: false, # optional
+ permissions: [], # optional
+ sleep: 0, # optional
+ width: 0, # optional
+ height: 0, # optional
+ quality: -1, # optional
+ output: ::JPG # optional
+)
diff --git a/docs/examples/1.8.x/server-swift/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-swift/examples/avatars/get-screenshot.md
new file mode 100644
index 0000000000..3aa1661093
--- /dev/null
+++ b/docs/examples/1.8.x/server-swift/examples/avatars/get-screenshot.md
@@ -0,0 +1,33 @@
+import Appwrite
+import AppwriteEnums
+
+let client = Client()
+ .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint
+ .setProject("") // Your project ID
+ .setSession("") // The user session to authenticate with
+
+let avatars = Avatars(client)
+
+let bytes = try await avatars.getScreenshot(
+ url: "https://example.com",
+ headers: [:], // optional
+ viewportWidth: 1, // optional
+ viewportHeight: 1, // optional
+ scale: 0.1, // optional
+ theme: .light, // optional
+ userAgent: "", // optional
+ fullpage: false, // optional
+ locale: "", // optional
+ timezone: .africaAbidjan, // optional
+ latitude: -90, // optional
+ longitude: -180, // optional
+ accuracy: 0, // optional
+ touch: false, // optional
+ permissions: [], // optional
+ sleep: 0, // optional
+ width: 0, // optional
+ height: 0, // optional
+ quality: -1, // optional
+ output: .jpg // optional
+)
+
diff --git a/docs/sdks/cli/CHANGELOG.md b/docs/sdks/cli/CHANGELOG.md
index ac1624401c..8e50441769 100644
--- a/docs/sdks/cli/CHANGELOG.md
+++ b/docs/sdks/cli/CHANGELOG.md
@@ -1,5 +1,9 @@
# Change Log
+## 11.1.1
+
+* Fix duplicate `enums` during type generation by prefixing them with table name. For example, `enum MyEnum` will now be generated as `enum MyTableMyEnum` to avoid conflicts.
+
## 11.1.0
* Add `total` parameter to list queries allowing skipping counting rows in a table for improved performance
diff --git a/docs/sdks/dart/CHANGELOG.md b/docs/sdks/dart/CHANGELOG.md
index 1a2cd6a5be..7fd7227f15 100644
--- a/docs/sdks/dart/CHANGELOG.md
+++ b/docs/sdks/dart/CHANGELOG.md
@@ -1,5 +1,12 @@
# Change Log
+## 19.4.0
+
+* Add `getScreenshot` method to `Avatars` service
+* Add enums `Theme`, `Output` and `Timezone`
+* Update runtime enums to add support for `dart39` and `flutter335` runtimes
+* Fix passing of `null` values and stripping only non-nullable optional parameters from the request body
+
## 19.3.0
* Add `total` parameter to list queries allowing skipping counting rows in a table for improved performance
diff --git a/docs/sdks/flutter/CHANGELOG.md b/docs/sdks/flutter/CHANGELOG.md
index 5ab7d3269a..2f26f34edd 100644
--- a/docs/sdks/flutter/CHANGELOG.md
+++ b/docs/sdks/flutter/CHANGELOG.md
@@ -1,5 +1,9 @@
# Change Log
+## 20.3.1
+
+* Fix passing of `null` values and stripping only non-nullable optional parameters from the request body
+
## 20.3.0
* Add `total` parameter to list queries allowing skipping counting rows in a table for improved performance
diff --git a/docs/sdks/php/CHANGELOG.md b/docs/sdks/php/CHANGELOG.md
index 6e8d4d7545..14a26e441d 100644
--- a/docs/sdks/php/CHANGELOG.md
+++ b/docs/sdks/php/CHANGELOG.md
@@ -1,5 +1,15 @@
# Change Log
+## 18.0.1
+
+* Fix `TablesDB` service to use correct file name
+
+## 18.0.0
+
+* Fix duplicate methods issue (e.g., `updateMFA` and `updateMfa`) causing build and runtime errors
+* Add support for `getScreenshot` method to `Avatars` service
+* Add `Output`, `Theme` and `Timezone` enums
+
## 17.5.0
* Add `total` parameter to list queries allowing skipping counting rows in a table for improved performance
diff --git a/phpunit.xml b/phpunit.xml
index 4c4e55ea4e..a8578995c1 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -31,6 +31,7 @@
./tests/e2e/Services/Locale
./tests/e2e/Services/Projects
./tests/e2e/Services/Storage
+ ./tests/e2e/Services/Tokens
./tests/e2e/Services/Webhooks
./tests/e2e/Services/Messaging
./tests/e2e/Services/Migrations
diff --git a/public/images/sites/templates/text-to-speech-dark.png b/public/images/sites/templates/text-to-speech-dark.png
new file mode 100644
index 0000000000..afa68c4227
Binary files /dev/null and b/public/images/sites/templates/text-to-speech-dark.png differ
diff --git a/public/images/sites/templates/text-to-speech-light.png b/public/images/sites/templates/text-to-speech-light.png
new file mode 100644
index 0000000000..e10148fe17
Binary files /dev/null and b/public/images/sites/templates/text-to-speech-light.png differ
diff --git a/src/Appwrite/Migration/Version/V23.php b/src/Appwrite/Migration/Version/V23.php
index 7a6d58d59f..a4027b506e 100644
--- a/src/Appwrite/Migration/Version/V23.php
+++ b/src/Appwrite/Migration/Version/V23.php
@@ -6,6 +6,7 @@ use Appwrite\Migration\Migration;
use Exception;
use Throwable;
use Utopia\CLI\Console;
+use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict;
@@ -132,6 +133,13 @@ class V23 extends Migration
}
$this->dbForProject->purgeCachedCollection($id);
break;
+ case 'migrations':
+ try {
+ $this->updateMigrateErrorSize();
+ } catch (\Throwable $th) {
+ Console::warning("Failed to migration error attribute size in collection {$id}: {$th->getMessage()}");
+ }
+
default:
break;
}
@@ -201,4 +209,46 @@ class V23 extends Migration
}
return $document;
}
+
+ /**
+ * Update migration attribute size
+ * @return void
+ */
+ private function updateMigrateErrorSize(): void
+ {
+
+ if ($this->project->getId() === 'console') {
+ return;
+ }
+
+ // Read-modify-write from the live schema to avoid overwriting unrelated changes.
+ $migration = $this->dbForProject->getCollection('migrations');
+ $attributes = $migration->getAttribute('attributes', []);
+ $attrsArray = \array_map(fn (Document $doc) => $doc->getArrayCopy(), $attributes);
+ $errorsIdx = \array_search('errors', \array_column($attrsArray, '$id'));
+
+ if ($errorsIdx === false) {
+ Console::warning("Skipping: 'errors' attribute not found in migrations collection for project {$this->project->getId()}");
+ return;
+ }
+
+ $desiredSize = 1_000_000;
+ $migrationAttributes = Config::getParam('collections', [])['projects']['migrations']['attributes'] ?? [];
+ $migrationIndex = \array_search('errors', \array_column($migrationAttributes, '$id'));
+
+ if ($migrationIndex !== false && isset($migrationAttributes[$migrationIndex]['size'])) {
+ $desiredSize = (int) $migrationAttributes[$migrationIndex]['size'];
+ }
+
+ $currentSize = (int) ($attributes[$errorsIdx]['size'] ?? 0);
+
+ if ($currentSize === $desiredSize) {
+ Console::warning("Skipping: 'errors' attribute already of desired size {$desiredSize} in migrations collection for project {$this->project->getId()}");
+ return;
+ }
+ $attributes[$errorsIdx]['size'] = $desiredSize;
+ $migration->setAttribute('attributes', $attributes);
+ $this->dbForProject->updateDocument($migration->getCollection(), $migration->getId(), $migration);
+ $this->dbForProject->purgeCachedCollection('migrations');
+ }
}
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php
index bbe84c56ee..00c29d6bba 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php
@@ -21,6 +21,7 @@ use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Request;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
+use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
class Create extends Base
@@ -65,7 +66,8 @@ class Create extends Base
->param('repository', '', new Text(128, 0), 'Repository name of the template.')
->param('owner', '', new Text(128, 0), 'The name of the owner of the template.')
->param('rootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.')
- ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.')
+ ->param('type', '', new WhiteList(['commit', 'branch', 'tag']), 'Type for the reference provided. Can be commit, branch, or tag')
+ ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag')
->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true)
->inject('request')
->inject('response')
@@ -83,7 +85,8 @@ class Create extends Base
string $repository,
string $owner,
string $rootDirectory,
- string $version,
+ string $type,
+ string $reference,
bool $activate,
Request $request,
Response $response,
@@ -100,11 +103,16 @@ class Create extends Base
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
+ $branchUrl = "https://github.com/$owner/$repository/blob/$reference";
+
+ $repositoryUrl = "https://github.com/$owner/$repository";
+
$template = new Document([
'repositoryName' => $repository,
'ownerName' => $owner,
'rootDirectory' => $rootDirectory,
- 'version' => $version
+ 'referenceType' => $type,
+ 'referenceValue' => $reference,
]);
if (!empty($function->getAttribute('providerRepositoryId'))) {
@@ -146,7 +154,12 @@ class Create extends Base
'resourceType' => 'functions',
'entrypoint' => $function->getAttribute('entrypoint', ''),
'buildCommands' => $function->getAttribute('commands', ''),
- 'type' => 'manual',
+ 'providerRepositoryName' => $repository,
+ 'providerRepositoryOwner' => $owner,
+ 'providerRepositoryUrl' => $repositoryUrl,
+ 'providerBranchUrl' => $branchUrl,
+ 'providerBranch' => $type == GitHub::CLONE_TYPE_BRANCH ? $reference : '',
+ 'type' => 'vcs',
'activate' => $activate,
]));
diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
index f9aa60db5f..22b302f26e 100644
--- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
+++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
@@ -310,20 +310,23 @@ class Builds extends Action
// Non-VCS + Template
$templateRepositoryName = $template->getAttribute('repositoryName', '');
$templateOwnerName = $template->getAttribute('ownerName', '');
- $templateVersion = $template->getAttribute('version', '');
+ $templateReferenceType = $template->getAttribute('referenceType', '');
+ $templateReferenceValue = $template->getAttribute('referenceValue', '');
$templateRootDirectory = $template->getAttribute('rootDirectory', '');
$templateRootDirectory = \rtrim($templateRootDirectory, '/');
$templateRootDirectory = \ltrim($templateRootDirectory, '.');
$templateRootDirectory = \ltrim($templateRootDirectory, '/');
- if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateVersion)) {
+ if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateReferenceType) && !empty($templateReferenceValue)) {
$stdout = '';
$stderr = '';
// Clone template repo
$tmpTemplateDirectory = '/tmp/builds/' . $deploymentId . '-template';
- $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory);
+
+ $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateReferenceValue, $templateReferenceType, $tmpTemplateDirectory, $templateRootDirectory);
+
$exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr);
if ($exit !== 0) {
diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php
index dc90045b0c..dc7d4c4ace 100644
--- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php
+++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php
@@ -23,6 +23,7 @@ use Utopia\Swoole\Request;
use Utopia\System\System;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
+use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
class Create extends Base
@@ -67,7 +68,8 @@ class Create extends Base
->param('repository', '', new Text(128, 0), 'Repository name of the template.')
->param('owner', '', new Text(128, 0), 'The name of the owner of the template.')
->param('rootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.')
- ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the site template.')
+ ->param('type', '', new WhiteList(['branch', 'commit', 'tag']), 'Type for the reference provided. Can be commit, branch, or tag')
+ ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag')
->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true)
->inject('request')
->inject('response')
@@ -85,7 +87,8 @@ class Create extends Base
string $repository,
string $owner,
string $rootDirectory,
- string $version,
+ string $type,
+ string $reference,
bool $activate,
Request $request,
Response $response,
@@ -102,11 +105,15 @@ class Create extends Base
throw new Exception(Exception::SITE_NOT_FOUND);
}
+ $branchUrl = "https://github.com/$owner/$repository/blob/$reference";
+ $repositoryUrl = "https://github.com/$owner/$repository";
+
$template = new Document([
'repositoryName' => $repository,
'ownerName' => $owner,
'rootDirectory' => $rootDirectory,
- 'version' => $version
+ 'referenceType' => $type,
+ 'referenceValue' => $reference
]);
if (!empty($site->getAttribute('providerRepositoryId'))) {
@@ -157,9 +164,14 @@ class Create extends Base
'resourceType' => 'sites',
'buildCommands' => \implode(' && ', $commands),
'buildOutput' => $site->getAttribute('outputDirectory', ''),
+ 'providerRepositoryName' => $repository,
+ 'providerRepositoryOwner' => $owner,
+ 'providerRepositoryUrl' => $repositoryUrl,
+ 'providerBranchUrl' => $branchUrl,
+ 'providerBranch' => $type == GitHub::CLONE_TYPE_BRANCH ? $reference : '',
'adapter' => $site->getAttribute('adapter', ''),
'fallbackFile' => $site->getAttribute('fallbackFile', ''),
- 'type' => 'manual',
+ 'type' => 'vcs',
'activate' => $activate,
]));
diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php
index fe7a0187e9..e4de4c1380 100644
--- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php
+++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php
@@ -61,7 +61,7 @@ class Create extends Action
))
->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).')
->param('fileId', '', new UID(), 'File unique ID.')
- ->param('expire', null, new Nullable(new DatetimeValidator()), 'Token expiry date', true)
+ ->param('expire', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Token expiry date', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
@@ -70,7 +70,6 @@ class Create extends Action
public function action(string $bucketId, string $fileId, ?string $expire, Response $response, Database $dbForProject, Event $queueForEvents): void
{
-
/**
* @var Document $bucket
* @var Document $file
diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php
index 7a15708011..fef2c38a81 100644
--- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php
+++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php
@@ -57,7 +57,7 @@ class Update extends Action
contentType: ContentType::JSON
))
->param('tokenId', '', new UID(), 'Token unique ID.')
- ->param('expire', null, new Nullable(new DatetimeValidator()), 'File token expiry date', true)
+ ->param('expire', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'File token expiry date', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php
index cf4f107e8e..d3c605655f 100644
--- a/src/Appwrite/Platform/Tasks/SDKs.php
+++ b/src/Appwrite/Platform/Tasks/SDKs.php
@@ -259,8 +259,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
}
if ($createRelease) {
- Console::execute('git config --global user.email "$GIT_EMAIL"', stdin: '', stdout: '', stderr: '');
-
$releaseVersion = $language['version'];
$repoName = $language['gitUserName'] . '/' . $language['gitRepoName'];
@@ -429,16 +427,21 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
mkdir -p ' . $target . ' && \
cd ' . $target . ' && \
git init && \
+ git config core.ignorecase false && \
+ git config pull.rebase false && \
git remote add origin ' . $gitUrl . ' && \
git fetch origin && \
- git checkout ' . $repoBranch . ' || git checkout -b ' . $repoBranch . ' && \
+ (git checkout -f ' . $repoBranch . ' 2>/dev/null || git checkout -b ' . $repoBranch . ') && \
git pull origin ' . $repoBranch . ' && \
- git checkout ' . $gitBranch . ' || git checkout -b ' . $gitBranch . ' && \
- git fetch origin ' . $gitBranch . ' || git push -u origin ' . $gitBranch . ' && \
- git pull origin ' . $gitBranch . ' && \
- find . -mindepth 1 ! -path "./.git*" -delete && \
+ (git checkout -f ' . $gitBranch . ' 2>/dev/null || git checkout -b ' . $gitBranch . ') && \
+ (git fetch origin ' . $gitBranch . ' 2>/dev/null || git push -u origin ' . $gitBranch . ') && \
+ git reset --hard origin/' . $gitBranch . ' 2>/dev/null || true && \
+ (test -d .github && cp -r .github /tmp/.github-backup-$$ || true) && \
+ git rm -rf --cached . && \
+ git clean -fdx -e .git -e .github && \
cp -r ' . $result . '/. ' . $target . '/ && \
- git add . && \
+ (test -d /tmp/.github-backup-$$ && cp -r /tmp/.github-backup-$$/.github . && rm -rf /tmp/.github-backup-$$ || true) && \
+ git add -A && \
git commit -m "' . $message . '" && \
git push -u origin ' . $gitBranch . '
');
diff --git a/src/Appwrite/Utopia/Request/Filters/V21.php b/src/Appwrite/Utopia/Request/Filters/V21.php
new file mode 100644
index 0000000000..3ef0becf1d
--- /dev/null
+++ b/src/Appwrite/Utopia/Request/Filters/V21.php
@@ -0,0 +1,34 @@
+convertVersionToTypeAndReference($content);
+ break;
+ }
+ return $content;
+ }
+
+ /**
+ * Convert version parameter to type and reference for backwards compatibility
+ * with 1.8.0 template deployment endpoints
+ */
+ protected function convertVersionToTypeAndReference(array $content): array
+ {
+ if (!empty($content['version'])) {
+ $content['type'] = 'tag';
+ $content['reference'] = $content['version'];
+ unset($content['version']);
+ }
+ return $content;
+ }
+}
diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php
index 8f5477331a..dc49d27aea 100644
--- a/tests/e2e/General/UsageTest.php
+++ b/tests/e2e/General/UsageTest.php
@@ -28,6 +28,7 @@ class UsageTest extends Scope
FunctionsBase::createVariable insteadof SitesBase;
FunctionsBase::getVariable insteadof SitesBase;
FunctionsBase::listVariables insteadof SitesBase;
+ FunctionsBase::helperGetLatestCommit insteadof SitesBase;
FunctionsBase::updateVariable insteadof SitesBase;
FunctionsBase::deleteVariable insteadof SitesBase;
FunctionsBase::getDeployment insteadof SitesBase;
diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php
index b2f85637a8..9f35932700 100644
--- a/tests/e2e/Services/Account/AccountBase.php
+++ b/tests/e2e/Services/Account/AccountBase.php
@@ -41,6 +41,11 @@ trait AccountBase
$this->assertNotEmpty($response['body']['accessedAt']);
$this->assertArrayHasKey('targets', $response['body']);
$this->assertEquals($email, $response['body']['targets'][0]['identifier']);
+ $this->assertArrayNotHasKey('emailCanonical', $response['body']);
+ $this->assertArrayNotHasKey('emailIsFree', $response['body']);
+ $this->assertArrayNotHasKey('emailIsDisposable', $response['body']);
+ $this->assertArrayNotHasKey('emailIsCorporate', $response['body']);
+ $this->assertArrayNotHasKey('emailIsCanonical', $response['body']);
/**
* Test for FAILURE
diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php
index 27b67d851d..7403b23a73 100644
--- a/tests/e2e/Services/Functions/FunctionsBase.php
+++ b/tests/e2e/Services/Functions/FunctionsBase.php
@@ -271,6 +271,29 @@ trait FunctionsBase
return $template;
}
+ protected function helperGetLatestCommit(string $owner, string $repository): ?string
+ {
+ $ch = curl_init("https://api.github.com/repos/{$owner}/{$repository}/commits/main");
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'User-Agent: Appwrite',
+ 'Accept: application/vnd.github.v3+json'
+ ]);
+
+ $response = curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($httpCode === 200) {
+ $commitData = json_decode($response, true);
+ if (isset($commitData['sha'])) {
+ return $commitData['sha'];
+ }
+ }
+
+ return null;
+ }
+
protected function createExecution(string $functionId, mixed $params = []): mixed
{
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([
diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
index f5846af959..8cc986b072 100644
--- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
+++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
@@ -361,7 +361,7 @@ class FunctionsCustomServerTest extends Scope
$starterTemplate = $this->getTemplate('starter');
$this->assertEquals(200, $starterTemplate['headers']['status-code']);
- $phpRuntime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) {
+ $runtime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) {
return $runtime['name'] === 'node-22';
}))[0];
@@ -374,15 +374,15 @@ class FunctionsCustomServerTest extends Scope
'name' => $starterTemplate['body']['name'],
'runtime' => 'node-22',
'execute' => $starterTemplate['body']['permissions'],
- 'entrypoint' => $phpRuntime['entrypoint'],
+ 'entrypoint' => $runtime['entrypoint'],
'events' => $starterTemplate['body']['events'],
'schedule' => $starterTemplate['body']['cron'],
'timeout' => $starterTemplate['body']['timeout'],
- 'commands' => $phpRuntime['commands'],
+ 'commands' => $runtime['commands'],
'scopes' => $starterTemplate['body']['scopes'],
'templateRepository' => $starterTemplate['body']['providerRepositoryId'],
'templateOwner' => $starterTemplate['body']['providerOwner'],
- 'templateRootDirectory' => $phpRuntime['providerRootDirectory'],
+ 'templateRootDirectory' => $runtime['providerRootDirectory'],
'templateVersion' => $starterTemplate['body']['providerVersion'],
]
);
@@ -399,19 +399,29 @@ class FunctionsCustomServerTest extends Scope
'activate' => true,
'repository' => $starterTemplate['body']['providerRepositoryId'],
'owner' => $starterTemplate['body']['providerOwner'],
- 'rootDirectory' => $phpRuntime['providerRootDirectory'],
- 'version' => $starterTemplate['body']['providerVersion'],
+ 'rootDirectory' => $runtime['providerRootDirectory'],
+ 'type' => 'tag',
+ 'reference' => $starterTemplate['body']['providerVersion'],
]
);
$this->assertEquals(202, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['$id']);
- $deployment = $this->getDeployment($functionId, $deployment['body']['$id']);
+ // Wait for deployment to be ready
+ $deploymentId = $deployment['body']['$id'];
+ $this->assertEventually(function () use ($functionId, $deploymentId) {
+ $deployment = $this->getDeployment($functionId, $deploymentId);
+ $this->assertEquals('ready', $deployment['body']['status']);
+ }, 50000, 500);
+
+ // Verify deployment sizes
+ $deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
- $this->assertEquals(0, $deployment['body']['sourceSize']);
- $this->assertEquals(0, $deployment['body']['buildSize']);
- $this->assertEquals(0, $deployment['body']['totalSize']);
+ $this->assertGreaterThan(0, $deployment['body']['sourceSize']);
+ $this->assertGreaterThan(0, $deployment['body']['buildSize']);
+ $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize'];
+ $this->assertEquals($totalSize, $deployment['body']['totalSize']);
$deployments = $this->listDeployments($functionId);
@@ -433,16 +443,7 @@ class FunctionsCustomServerTest extends Scope
$lastDeployment = $deployments['body']['deployments'][0];
$this->assertNotEmpty($lastDeployment['$id']);
- $this->assertEquals(0, $lastDeployment['sourceSize']);
-
- $deploymentId = $lastDeployment['$id'];
-
- $this->assertEventually(function () use ($functionId, $deploymentId) {
- $deployment = $this->getDeployment($functionId, $deploymentId);
-
- $this->assertEquals(200, $deployment['headers']['status-code']);
- $this->assertEquals('ready', $deployment['body']['status']);
- }, 50000, 1000);
+ $this->assertGreaterThan(0, $lastDeployment['sourceSize']);
$function = $this->getFunction($functionId);
@@ -511,7 +512,144 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals($deployment['body']['$id'], $function['body']['deploymentId']);
$this->assertEquals($deployment['body']['$createdAt'], $function['body']['deploymentCreatedAt']);
- $function = $this->cleanupFunction($functionId);
+ $this->cleanupFunction($functionId);
+ }
+
+ public function testCreateFunctionAndDeploymentFromTemplateBranch()
+ {
+ $starterTemplate = $this->getTemplate('starter');
+ $this->assertEquals(200, $starterTemplate['headers']['status-code']);
+
+ $runtime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) {
+ return $runtime['name'] === 'node-22';
+ }))[0];
+
+ // If this fails, the template has variables, and this test needs to be updated
+ $this->assertEmpty($starterTemplate['body']['variables']);
+
+ $function = $this->createFunction(
+ [
+ 'functionId' => ID::unique(),
+ 'name' => $starterTemplate['body']['name'] . ' - Branch Test',
+ 'runtime' => 'node-22',
+ 'execute' => $starterTemplate['body']['permissions'],
+ 'entrypoint' => $runtime['entrypoint'],
+ 'events' => $starterTemplate['body']['events'],
+ 'schedule' => $starterTemplate['body']['cron'],
+ 'timeout' => $starterTemplate['body']['timeout'],
+ 'commands' => $runtime['commands'],
+ 'scopes' => $starterTemplate['body']['scopes'],
+ ]
+ );
+
+ $this->assertEquals(201, $function['headers']['status-code']);
+ $this->assertNotEmpty($function['body']['$id']);
+
+ $functionId = $function['body']['$id'] ?? '';
+
+ // Deploy using branch
+ $deployment = $this->createTemplateDeployment(
+ $functionId,
+ [
+ 'resourceId' => ID::unique(),
+ 'activate' => true,
+ 'repository' => $starterTemplate['body']['providerRepositoryId'],
+ 'owner' => $starterTemplate['body']['providerOwner'],
+ 'rootDirectory' => $runtime['providerRootDirectory'],
+ 'type' => 'branch',
+ 'reference' => 'main',
+ ]
+ );
+
+ $this->assertEquals(202, $deployment['headers']['status-code']);
+ $this->assertNotEmpty($deployment['body']['$id']);
+
+ $deploymentId = $deployment['body']['$id'];
+ $this->assertEventually(function () use ($functionId, $deploymentId) {
+ $deployment = $this->getDeployment($functionId, $deploymentId);
+ $this->assertEquals('ready', $deployment['body']['status']);
+ }, 50000, 500);
+
+ $deployment = $this->getDeployment($functionId, $deploymentId);
+ $this->assertEquals(200, $deployment['headers']['status-code']);
+ $this->assertGreaterThan(0, $deployment['body']['sourceSize']);
+ $this->assertGreaterThan(0, $deployment['body']['buildSize']);
+ $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize'];
+ $this->assertEquals($totalSize, $deployment['body']['totalSize']);
+
+ $this->cleanupFunction($functionId);
+ }
+
+ public function testCreateFunctionAndDeploymentFromTemplateCommit()
+ {
+ $starterTemplate = $this->getTemplate('starter');
+ $this->assertEquals(200, $starterTemplate['headers']['status-code']);
+
+ // Get latest commit using helper function
+ $latestCommit = $this->helperGetLatestCommit(
+ $starterTemplate['body']['providerOwner'],
+ $starterTemplate['body']['providerRepositoryId']
+ );
+ $this->assertNotNull($latestCommit);
+
+ $runtime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) {
+ return $runtime['name'] === 'node-22';
+ }))[0];
+
+ // If this fails, the template has variables, and this test needs to be updated
+ $this->assertEmpty($starterTemplate['body']['variables']);
+
+ $function = $this->createFunction(
+ [
+ 'functionId' => ID::unique(),
+ 'name' => $starterTemplate['body']['name'] . ' - Commit Test',
+ 'runtime' => 'node-22',
+ 'execute' => $starterTemplate['body']['permissions'],
+ 'entrypoint' => $runtime['entrypoint'],
+ 'events' => $starterTemplate['body']['events'],
+ 'schedule' => $starterTemplate['body']['cron'],
+ 'timeout' => $starterTemplate['body']['timeout'],
+ 'commands' => $runtime['commands'],
+ 'scopes' => $starterTemplate['body']['scopes'],
+ ]
+ );
+
+ $this->assertEquals(201, $function['headers']['status-code']);
+ $this->assertNotEmpty($function['body']['$id']);
+
+ $functionId = $function['body']['$id'] ?? '';
+
+ // Deploy using commit
+ $deployment = $this->createTemplateDeployment(
+ $functionId,
+ [
+ 'resourceId' => ID::unique(),
+ 'activate' => true,
+ 'repository' => $starterTemplate['body']['providerRepositoryId'],
+ 'owner' => $starterTemplate['body']['providerOwner'],
+ 'rootDirectory' => $runtime['providerRootDirectory'],
+ 'type' => 'commit',
+ 'reference' => $latestCommit,
+ ]
+ );
+
+ $this->assertEquals(202, $deployment['headers']['status-code']);
+ $this->assertNotEmpty($deployment['body']['$id']);
+
+ $deploymentId = $deployment['body']['$id'];
+ $this->assertEventually(function () use ($functionId, $deploymentId) {
+ $deployment = $this->getDeployment($functionId, $deploymentId);
+ $this->assertEquals('ready', $deployment['body']['status']);
+ }, 50000, 500);
+
+ $deployment = $this->getDeployment($functionId, $deploymentId);
+ $this->assertEquals(200, $deployment['headers']['status-code']);
+ $this->assertGreaterThan(0, $deployment['body']['sourceSize']);
+ $this->assertGreaterThan(0, $deployment['body']['buildSize']);
+ $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize'];
+ $this->assertEquals($totalSize, $deployment['body']['totalSize']);
+
+ $this->cleanupFunction($functionId);
}
/**
diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php
index 93c55b82b7..7eb5d9699c 100644
--- a/tests/e2e/Services/Sites/SitesBase.php
+++ b/tests/e2e/Services/Sites/SitesBase.php
@@ -329,9 +329,33 @@ trait SitesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
+
return $template;
}
+ protected function helperGetLatestCommit(string $owner, string $repository): ?string
+ {
+ $ch = curl_init("https://api.github.com/repos/{$owner}/{$repository}/commits/main");
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'User-Agent: Appwrite',
+ 'Accept: application/vnd.github.v3+json'
+ ]);
+
+ $response = curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($httpCode === 200) {
+ $commitData = json_decode($response, true);
+ if (isset($commitData['sha'])) {
+ return $commitData['sha'];
+ }
+ }
+
+ return null;
+ }
+
protected function deleteSite(string $siteId): mixed
{
$site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([
diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php
index 8591514796..8c03ec7649 100644
--- a/tests/e2e/Services/Sites/SitesCustomServerTest.php
+++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php
@@ -1567,7 +1567,157 @@ class SitesCustomServerTest extends Scope
'repository' => $template['providerRepositoryId'],
'owner' => $template['providerOwner'],
'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'],
- 'version' => $template['providerVersion'],
+ 'type' => 'tag',
+ 'reference' => $template['providerVersion'],
+ 'activate' => true
+ ]);
+
+ $this->assertEquals(202, $deployment['headers']['status-code']);
+ $this->assertNotEmpty($deployment['body']['$id']);
+
+ $deployment = $this->getDeployment($siteId, $deployment['body']['$id']);
+ $this->assertEquals(200, $deployment['headers']['status-code']);
+ $this->assertEquals(0, $deployment['body']['sourceSize']);
+ $this->assertEquals(0, $deployment['body']['buildSize']);
+ $this->assertEquals(0, $deployment['body']['totalSize']);
+
+ $this->assertEventually(function () use ($siteId) {
+ $site = $this->getSite($siteId);
+ $this->assertNotEmpty($site['body']['deploymentId']);
+ }, 50000, 500);
+
+ $domain = $this->setupSiteDomain($siteId);
+ $proxyClient = new Client();
+ $proxyClient->setEndpoint('http://' . $domain);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/');
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertStringContainsString("Astro Blog", $response['body']);
+ $this->assertStringContainsString("Hello, Astronaut!", $response['body']);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/about');
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertStringContainsString("Astro Blog", $response['body']);
+ $this->assertStringContainsString("About Me", $response['body']);
+
+ $deployment = $this->getDeployment($siteId, $deployment['body']['$id']);
+ $this->assertEquals(200, $deployment['headers']['status-code']);
+ $this->assertGreaterThan(0, $deployment['body']['sourceSize']);
+ $this->assertGreaterThan(0, $deployment['body']['buildSize']);
+ $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize'];
+ $this->assertEquals($totalSize, $deployment['body']['totalSize']);
+
+ $this->cleanupSite($siteId);
+ }
+
+ public function testCreateSiteFromTemplateBranch()
+ {
+ $template = $this->getTemplate('playground-for-astro');
+ $this->assertEquals(200, $template['headers']['status-code']);
+
+ $template = $template['body'];
+
+ $siteId = $this->setupSite([
+ 'siteId' => ID::unique(),
+ 'name' => 'Astro Blog - Branch Test',
+ 'framework' => $template['frameworks'][0]['key'],
+ 'adapter' => $template['frameworks'][0]['adapter'],
+ 'buildRuntime' => $template['frameworks'][0]['buildRuntime'],
+ 'outputDirectory' => $template['frameworks'][0]['outputDirectory'],
+ 'buildCommand' => $template['frameworks'][0]['buildCommand'],
+ 'installCommand' => $template['frameworks'][0]['installCommand'],
+ 'fallbackFile' => $template['frameworks'][0]['fallbackFile'],
+ ]);
+
+ $this->assertNotEmpty($siteId);
+
+ // Deploy using branch
+ $deployment = $this->createTemplateDeployment($siteId, [
+ 'repository' => $template['providerRepositoryId'],
+ 'owner' => $template['providerOwner'],
+ 'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'],
+ 'type' => 'branch',
+ 'reference' => 'main',
+ 'activate' => true
+ ]);
+
+ $this->assertEquals(202, $deployment['headers']['status-code']);
+ $this->assertNotEmpty($deployment['body']['$id']);
+
+ $deployment = $this->getDeployment($siteId, $deployment['body']['$id']);
+ $this->assertEquals(200, $deployment['headers']['status-code']);
+ $this->assertEquals(0, $deployment['body']['sourceSize']);
+ $this->assertEquals(0, $deployment['body']['buildSize']);
+ $this->assertEquals(0, $deployment['body']['totalSize']);
+
+ $this->assertEventually(function () use ($siteId) {
+ $site = $this->getSite($siteId);
+ $this->assertNotEmpty($site['body']['deploymentId']);
+ }, 50000, 500);
+
+ $domain = $this->setupSiteDomain($siteId);
+ $proxyClient = new Client();
+ $proxyClient->setEndpoint('http://' . $domain);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/');
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertStringContainsString("Astro Blog", $response['body']);
+ $this->assertStringContainsString("Hello, Astronaut!", $response['body']);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/about');
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertStringContainsString("Astro Blog", $response['body']);
+ $this->assertStringContainsString("About Me", $response['body']);
+
+ $deployment = $this->getDeployment($siteId, $deployment['body']['$id']);
+ $this->assertEquals(200, $deployment['headers']['status-code']);
+ $this->assertGreaterThan(0, $deployment['body']['sourceSize']);
+ $this->assertGreaterThan(0, $deployment['body']['buildSize']);
+ $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize'];
+ $this->assertEquals($totalSize, $deployment['body']['totalSize']);
+
+ $this->cleanupSite($siteId);
+ }
+
+ public function testCreateSiteFromTemplateCommit()
+ {
+ $template = $this->getTemplate('playground-for-astro');
+ $this->assertEquals(200, $template['headers']['status-code']);
+
+ // Get latest commit using helper function
+ $latestCommit = $this->helperGetLatestCommit(
+ $template['body']['providerOwner'],
+ $template['body']['providerRepositoryId']
+ );
+ $this->assertNotNull($latestCommit);
+
+ $template = $template['body'];
+
+ $siteId = $this->setupSite([
+ 'siteId' => ID::unique(),
+ 'name' => 'Astro Blog - Commit Test',
+ 'framework' => $template['frameworks'][0]['key'],
+ 'adapter' => $template['frameworks'][0]['adapter'],
+ 'buildRuntime' => $template['frameworks'][0]['buildRuntime'],
+ 'outputDirectory' => $template['frameworks'][0]['outputDirectory'],
+ 'buildCommand' => $template['frameworks'][0]['buildCommand'],
+ 'installCommand' => $template['frameworks'][0]['installCommand'],
+ 'fallbackFile' => $template['frameworks'][0]['fallbackFile'],
+ ]);
+
+ $this->assertNotEmpty($siteId);
+
+ // Deploy using commit
+ $deployment = $this->createTemplateDeployment($siteId, [
+ 'repository' => $template['providerRepositoryId'],
+ 'owner' => $template['providerOwner'],
+ 'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'],
+ 'type' => 'commit',
+ 'reference' => $latestCommit,
'activate' => true
]);
diff --git a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php
index c0f94a55bf..f1480faba0 100644
--- a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php
+++ b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php
@@ -9,7 +9,6 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
-use Utopia\Database\DateTime;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
@@ -63,10 +62,23 @@ class TokensConsoleClientTest extends Scope
$fileId = $file['body']['$id'];
+ // Failure case: Expire date is in the past
$token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
- ], $this->getHeaders()));
+ ], $this->getHeaders()), [
+ 'expire' => '2022-11-02',
+ ]);
+ $this->assertEquals(400, $token['headers']['status-code']);
+ $this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']);
+
+ // Success case: No expire date
+ $token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()), [
+ 'expire' => null,
+ ]);
$this->assertEquals(201, $token['headers']['status-code']);
$this->assertEquals('files', $token['body']['resourceType']);
@@ -107,8 +119,19 @@ class TokensConsoleClientTest extends Scope
{
$tokenId = $data['tokenId'];
+ // Failure case: Expire date is in the past
+ $token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'expire' => '2022-11-02',
+ ]);
+ $this->assertEquals(400, $token['headers']['status-code']);
+ $this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']);
+
// Finite expiry
- $expiry = DateTime::addSeconds(new \DateTime(), 3600);
+ $expiry = date('Y-m-d', strtotime("tomorrow"));
$token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
diff --git a/tests/e2e/Services/Tokens/TokensCustomServerTest.php b/tests/e2e/Services/Tokens/TokensCustomServerTest.php
index fe8fa2bad9..779d5449b3 100644
--- a/tests/e2e/Services/Tokens/TokensCustomServerTest.php
+++ b/tests/e2e/Services/Tokens/TokensCustomServerTest.php
@@ -7,7 +7,6 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
-use Utopia\Database\DateTime;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
@@ -61,6 +60,17 @@ class TokensCustomServerTest extends Scope
$fileId = $file['body']['$id'];
+ // Failure case: Expire date is in the past
+ $token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()), [
+ 'expire' => '2022-11-02',
+ ]);
+ $this->assertEquals(400, $token['headers']['status-code']);
+ $this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']);
+
+ // Success case: No expire date
$token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
@@ -83,8 +93,19 @@ class TokensCustomServerTest extends Scope
{
$tokenId = $data['tokenId'];
- // Finite expiry
- $expiry = DateTime::addSeconds(new \DateTime(), 3600);
+ // Failure case: Expire date is in the past
+ $token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'expire' => '2022-11-02',
+ ]);
+ $this->assertEquals(400, $token['headers']['status-code']);
+ $this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']);
+
+ // Success case: Finite expiry
+ $expiry = date('Y-m-d', strtotime("tomorrow"));
$token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -94,9 +115,10 @@ class TokensCustomServerTest extends Scope
]);
$dateValidator = new DatetimeValidator();
+ $this->assertEquals(200, $token['headers']['status-code']);
$this->assertTrue($dateValidator->isValid($token['body']['expire']));
- // Infinite expiry
+ // Success case: Infinite expiry
$token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],