Compare commits

...
Author SHA1 Message Date
Chirag Aggarwal cc7b5b7dcf fix: missing nullable in datetime 2025-11-14 15:05:32 +05:30
Jake BarnbyandGitHub a1dff370a4 Merge pull request #10815 from appwrite/fix-response-models-multi-methods
fix: generation of all response models in case of multi method endpoints
2025-11-14 06:40:50 +00:00
Jake BarnbyandGitHub d0057ec404 Merge pull request #10800 from appwrite/feat-operators
Fix operators in transactions
2025-11-14 05:13:52 +00:00
Jake Barnby ab90563989 Fix tests 2025-11-14 17:31:17 +13:00
Jake BarnbyandGitHub e783f4c234 Merge pull request #10813 from appwrite/feat-csv-export
Feat csv export
2025-11-14 04:16:31 +00:00
Jake Barnby 082afcec19 Merge branch 'feat-operators' of github.com:appwrite/appwrite into feat-operators 2025-11-14 16:40:24 +13:00
Jake Barnby 2ab36f4cd6 Use helper for tests 2025-11-14 16:40:00 +13:00
Jake Barnby 72c71785a6 Fix doc 2025-11-14 16:35:10 +13:00
Jake BarnbyandGitHub 463eb0e66a Merge pull request #10785 from appwrite/dat-810
Added error message for the backups route
2025-11-14 03:30:35 +00:00
Jake Barnby 0588fb405d Fix keys 2025-11-14 16:16:06 +13:00
Jake Barnby b9211d7141 Add delete old CSV job for maintenance 2025-11-14 15:55:41 +13:00
Jake Barnby a11cf6b002 Merge remote-tracking branch 'origin/1.8.x' into feat-csv-export 2025-11-14 15:34:36 +13:00
Luke B. SilverandGitHub a2799b16f8 Merge pull request #10778 from appwrite/fix-nullable-validation
fix: null validation for optional params
2025-11-13 11:39:23 +00:00
Jake BarnbyandGitHub 298e6f9260 Merge branch '1.8.x' into feat-operators 2025-11-13 11:29:05 +00:00
Jake Barnby 3df2efb7ae Fix file reads 2025-11-14 00:15:18 +13:00
Jake BarnbyandGitHub b0a0454c55 Merge pull request #10738 from appwrite/fix-webp-library
Fix webp upload and previews
2025-11-13 11:01:10 +00:00
Jake Barnby b2d228e102 Update specs 2025-11-13 23:42:13 +13:00
Chirag Aggarwal b695ddc7e3 Merge branch '1.8.x' into fix-nullable-validation 2025-11-13 16:06:33 +05:30
Chirag Aggarwal 3bbd87ecb2 fix: openapi too 2025-11-13 16:05:27 +05:30
Chirag Aggarwal fe8c4cb479 fix: generation of all response models in case of multi method endpoints 2025-11-13 15:59:12 +05:30
Matej BačoandGitHub 5e4c74d501 Merge pull request #10486 from appwrite/feat-add-branch-deployments
Add branch-only deployments to Sites without ref tags
2025-11-13 10:33:46 +01:00
Jake Barnby c78da61304 Merge remote-tracking branch 'origin/1.8.x' into feat-csv-export 2025-11-13 22:14:10 +13:00
Matej BačoandGitHub fe8593fecc Merge pull request #10751 from appwrite/ser-279
fix: Throw error when file token expiry is in the past
2025-11-13 09:34:46 +01:00
Damodar LohaniandGitHub b07674ccd3 Merge pull request #10812 from appwrite/fix-upgrade-utopia-database
Upgrade utopia-php database
2025-11-13 13:31:29 +05:45
Damodar Lohani aa5e3bf9a9 Merge remote-tracking branch 'origin/1.8.x' into fix-upgrade-utopia-database 2025-11-13 07:00:45 +00:00
Damodar Lohani c1b50e9941 Upgrade utopia-php database 2025-11-13 06:59:03 +00:00
Jake BarnbyandGitHub 3a76249d80 Merge pull request #10811 from appwrite/release-flutter-dart-add-screenshot-examples
release flutter + dart and add screenshot api docs
2025-11-13 06:58:32 +00:00
Chirag Aggarwal b2991709dc update version 2025-11-13 11:38:50 +05:30
Chirag Aggarwal fcc07533e1 update changelog 2025-11-13 11:34:35 +05:30
Chirag Aggarwal 3a6d907b42 release flutter + dart and add screenshot api docs 2025-11-13 11:29:38 +05:30
Atharva Deosthale d411b741b3 address comments 2025-11-12 23:06:18 +05:30
Hemachandar bc19e66775 fix tests 2025-11-12 20:27:42 +05:30
Hemachandar 08c9db7c60 use inline validator 2025-11-12 19:23:57 +05:30
Jake Barnby e8f2e78ac4 Use internal bucket 2025-11-12 22:18:25 +13:00
Jake Barnby 37f16ef0bb Fix array conversion 2025-11-12 21:54:37 +13:00
Jake Barnby 46d785bbb8 Fix tests 2025-11-12 21:04:17 +13:00
Damodar LohaniandGitHub a9c9f602ec Merge pull request #10807 from appwrite/feat-update-flutter-default-build-runtime
Update Flutter framework default build runtime version to 3.35
2025-11-12 13:16:20 +05:45
Damodar LohaniandGitHub d90b4c2f19 update framework config 2025-11-12 12:43:34 +05:45
Damodar LohaniandGitHub 79d733486c Update Flutter framework default build runtime version to 3.35 2025-11-12 12:35:31 +05:45
Damodar LohaniandGitHub d862245090 Merge pull request #10806 from appwrite/lohanidamodar-patch-2
Fix: add default fallback to Flutter framework
2025-11-12 12:30:11 +05:45
Damodar LohaniandGitHub aacaadcce0 Fix: add default fallback to Flutter framework 2025-11-12 12:17:15 +05:45
Damodar LohaniandGitHub 69ee6e0659 Merge pull request #10805 from appwrite/lohanidamodar-patch-2
Feat: enable Flutter 3.35 and Dart 3.9 for sites
2025-11-12 10:51:00 +05:45
Jake Barnby e564793089 Merge remote-tracking branch 'origin/1.8.x' into feat-operators
# Conflicts:
#	composer.lock
2025-11-12 17:02:58 +13:00
Jake Barnby c9582978ed Fix operator parsing on txn ops 2025-11-12 16:48:42 +13:00
Jake Barnby ae21fadf0d Fix event firing on txn ops 2025-11-12 16:48:26 +13:00
Damodar LohaniandGitHub cfd62cf0a1 Update template-runtimes.php 2025-11-12 09:32:12 +05:45
Damodar LohaniandGitHub 279682941a enable dart 2025-11-12 09:31:04 +05:45
Damodar LohaniandGitHub eae26d90af Feat: enable Flutter 3.35 for sites 2025-11-12 09:25:35 +05:45
Jake BarnbyandGitHub b46ebb95a3 Merge pull request #10688 from appwrite/users-add-attributes
Users add new email attributes
2025-11-12 03:19:24 +00:00
Damodar LohaniandGitHub ffcb6a4677 Merge pull request #10804 from appwrite/feat-upgrade-runtimes
Feat: upgrade runtime
2025-11-12 07:26:17 +05:45
Damodar LohaniandGitHub 142e359dba Merge branch '1.8.x' into feat-upgrade-runtimes 2025-11-12 07:16:09 +05:45
Jake BarnbyandGitHub 4130983210 Merge pull request #10407 from appwrite/dat-647
updated migration error attribute size
2025-11-12 01:05:13 +00:00
Damodar Lohani e01ac65ce8 Feat: upgrade runtime 2025-11-12 00:26:27 +00:00
fogelito 97454c0768 EmailValidator chnage to email 2025-11-11 15:33:16 +02:00
fogelito 8b4a56cd9b $emailCanonical 2025-11-11 15:25:10 +02:00
fogelito da0a6b167f EmailValidator 2025-11-11 15:00:09 +02:00
fogelito 27d0c511ac patch users email 2025-11-11 12:09:40 +02:00
fogelito 8a58f7b6d7 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into users-add-attributes 2025-11-11 11:36:01 +02:00
fogelito cc97d78b5a Add try catch 2025-11-11 11:35:18 +02:00
Luke B. SilverandGitHub 91c24b671b Merge pull request #10791 from appwrite/release-cli-php
chore: release cli and php
2025-11-11 09:22:57 +00:00
fogelito b28e536967 Add try catch 2025-11-11 10:35:39 +02:00
Jake Barnby ab5aac00c4 Format 2025-11-11 21:25:21 +13:00
Jake Barnby 353e84bb74 Fix keys 2025-11-11 21:10:11 +13:00
Jake Barnby 94614d32d0 Handle deep nested ops 2025-11-11 21:09:12 +13:00
Jake Barnby 1efe9fa693 Merge remote-tracking branch 'origin/1.8.x' into feat-operators
# Conflicts:
#	composer.lock
2025-11-11 20:21:15 +13:00
Jake Barnby 74ff9a955b Fix transactions + operators 2025-11-11 20:20:08 +13:00
fogelito ce7f40dd34 Revert response model 2025-11-11 08:16:09 +02:00
fogelito 49318b5dac Merge branch '1.8.x' of https://github.com/appwrite/appwrite into users-add-attributes 2025-11-11 08:09:11 +02:00
Chirag AggarwalandGitHub da736631b2 Merge branch '1.8.x' into release-cli-php 2025-11-11 10:55:52 +05:30
Matej BačoandGitHub 9db6f9cb75 Merge pull request #10782 from appwrite/add-elevenlabs-site-template
Add ElevenLabs text-to-speech sites template
2025-11-10 16:35:26 +01:00
fogelito 6096c42ff9 More updates 2025-11-10 17:30:40 +02:00
Aditya Oberai b06354b99a update tanstack starter and elevenlabs template scores 2025-11-10 15:12:20 +00:00
Aditya OberaiandGitHub a797951bec Merge branch '1.8.x' into add-elevenlabs-site-template 2025-11-10 20:38:21 +05:30
bb0007a1e8 Update app/config/templates/site.php
Co-authored-by: Matej Bačo <matejbacocom@gmail.com>
2025-11-10 20:38:07 +05:30
Aditya Oberai 507717fdf7 generate screenshots from docker command 2025-11-10 14:49:44 +00:00
fogelito 5afae248ae Test Update 2025-11-10 15:00:18 +02:00
fogelito 98255fe628 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into users-add-attributes
# Conflicts:
#	composer.lock
2025-11-10 14:52:25 +02:00
Chirag Aggarwal 057397197b update to 18.0.1 2025-11-10 18:20:31 +05:30
Aditya Oberai c398d5fba9 Add default value for ELEVENLABS_API_KEY env variable 2025-11-10 12:17:29 +00:00
Chirag Aggarwal 4dc700927e fix: examples 2025-11-10 15:39:54 +05:30
Chirag Aggarwal 4a148e6ef9 remove broken execute 2025-11-10 15:06:22 +05:30
Chirag Aggarwal 2ff0ae61c8 update php 2025-11-10 15:00:09 +05:30
Chirag Aggarwal b06460b562 chore: release cli and php 2025-11-10 14:19:26 +05:30
Chirag Aggarwal 74492ded44 update description 2025-11-10 11:35:25 +05:30
Chirag AggarwalandGitHub 9ecffbf4e9 Merge branch '1.8.x' into fix-nullable-validation 2025-11-10 10:19:13 +05:30
Chirag Aggarwal 0ed4b5dbe7 fix imports + add spec changes 2025-11-10 10:16:58 +05:30
Chirag Aggarwal eb17ffd5d2 wrap all optional values with default null with nullable 2025-11-10 10:15:49 +05:30
Jake BarnbyandGitHub 0bf8cead51 Merge pull request #10789 from appwrite/sync-specs
chore: sync specs
2025-11-10 04:33:40 +00:00
Damodar LohaniandGitHub add1142d1f Merge pull request #10786 from appwrite/feat-function-sites-stats
Feat: stats sites and functions runtimes and frameworks
2025-11-10 09:52:36 +05:45
Chirag Aggarwal 86896a6d3c chore: sync specs 2025-11-10 08:51:56 +05:30
Damodar Lohani 4fd4af312d Fix array wrong reference 2025-11-09 08:18:37 +00:00
Damodar Lohani 4b7a7496ff Feat: stats sites and functions runtimes and frameworks
- Sites frameworks count
- Functions runtimes count
2025-11-09 07:17:40 +00:00
ArnabChatterjee20k eb0e42b79c added error message for the backups route 2025-11-09 12:13:08 +05:30
Aditya OberaiandGitHub 0c93e7d545 Update app/config/templates/site.php 2025-11-07 23:29:36 +05:30
Aditya Oberai 469d0859fc Add ElevenLabs text-to-speech sites template 2025-11-07 17:07:25 +00:00
ArnabChatterjee20k 7c408d675e pr followups 2025-11-07 14:30:22 +05:30
Luke B. SilverandGitHub 4a96d89ee6 Merge pull request #10765 from appwrite/feat-sdk-release-runtime-config
Set configs for SDK release at runtime
2025-11-07 08:57:43 +00:00
ArnabChatterjee20k 4d47bc2ebf Merge remote-tracking branch 'upstream/1.8.x' into dat-647 2025-11-07 13:48:59 +05:30
ArnabChatterjee20k 119d6c1789 Merge remote-tracking branch 'upstream/1.8.x' into dat-647 2025-11-07 13:19:28 +05:30
Steven NguyenandGitHub 2aa2b8df91 chore: set GITHUB_TOKEN and git email at runtime 2025-11-07 00:06:01 +00:00
Steven NguyenandGitHub 4f1940ac0c Merge pull request #10739 from appwrite/copilot/sub-pr-10738
Add test coverage for webp upload and preview functionality
2025-11-06 15:07:52 -08:00
Steven NguyenandGitHub ef938a1c39 Merge pull request #10732 from appwrite/copilot/fix-google-token-disappears
Fix Google OAuth2 refresh token support
2025-11-06 10:28:02 -08:00
Steven NguyenandGitHub ebee879855 Merge pull request #10748 from appwrite/copilot/update-issue-triage-workflow
Migrate issue-triage workflow from event-based to scheduled batch processing with duplicate detection
2025-11-06 08:14:25 -08:00
Matej BačoandGitHub e342009c9e Merge pull request #10771 from appwrite/Update-facts-on-Github-sites-and-functions
feat: Add new tips to Comment class
2025-11-06 13:50:27 +01:00
Matej BačoandGitHub 41d89f1b7a Merge pull request #10773 from appwrite/chore-remove-locale-cache
Chore: remove locale cache
2025-11-06 13:49:32 +01:00
Matej Bačo ad6aff6af2 remove locale cache 2025-11-06 13:24:17 +01:00
Harsh MahajanandGitHub 9b52f6f8f6 Add new tips to Comment class 2025-11-06 14:17:31 +05:30
Matej BačoandGitHub ad46d057d5 Merge pull request #10593 from appwrite/feat-SER-193-Update-facts-on-github-sites-and-functions
Feat: update facts on GitHub sites and functions
2025-11-06 09:35:53 +01:00
copilot-swe-agent[bot]andstnguyen90 8a9d0a3f33 Rename logo-after.webp to image.webp
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-11-05 19:30:21 +00:00
fogelito 1f0e3e0f51 lock file 2025-11-05 13:21:52 +02:00
Steven NguyenandGitHub c910c98b32 chore: install ssh and gh commands for dev
This will allow creating github releases for SDKs
2025-11-04 16:18:49 +00:00
fogelito fe9d49c346 Response Model 2025-11-04 17:13:53 +02:00
Matej BačoandGitHub 6c1e4434a6 Merge pull request #10763 from appwrite/update-readme-file
Update README.md
2025-11-04 15:52:25 +01:00
VeereshandGitHub ba6e389f81 Update README.md 2025-11-04 19:33:30 +05:30
copilot-swe-agent[bot]andstnguyen90 2d332264c6 Replace with simple webp image for testing
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-11-04 06:16:41 +00:00
copilot-swe-agent[bot]andstnguyen90 2f843153da Add duplicate/related issues detection and listing to triage workflow
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-11-03 20:02:03 +00:00
copilot-swe-agent[bot]andstnguyen90 df8fb23e75 Fix test assertion for file count with offset
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-11-03 18:13:21 +00:00
Hemachandar 60e57eb9c5 lint 2025-11-03 17:29:48 +05:30
Hemachandar 10a9845898 fix: Throw error when file token expiry is in the past 2025-11-03 16:55:13 +05:30
fogelito 6a41f8b2b9 Remove todos 2025-11-02 11:16:12 +02:00
fogelito 503145734b Move user Document out of try block 2025-11-02 11:06:56 +02:00
fogelito 371cab0658 try catch EmailCanonical 2025-11-02 10:59:28 +02:00
fogelito ca8cb6f260 Add utopia-php/emails 2025-11-02 09:13:55 +02:00
copilot-swe-agent[bot]andstnguyen90 1f9afc8cab Update issue-triage workflow to run daily and process last 24h issues
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-11-01 15:47:43 +00:00
copilot-swe-agent[bot] c5d4551cab Initial plan 2025-11-01 15:39:50 +00:00
copilot-swe-agent[bot]andstnguyen90 d1511d5ba1 Refactor webp tests per review feedback
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-10-30 22:26:30 +00:00
copilot-swe-agent[bot]andstnguyen90 4a8651ab39 Add webp test cases for upload/view and preview
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-10-30 21:58:02 +00:00
copilot-swe-agent[bot]andstnguyen90 de4f473da1 Initial plan for adding webp test cases
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-10-30 21:56:12 +00:00
copilot-swe-agent[bot] ce539972a5 Initial plan 2025-10-30 21:51:50 +00:00
Steven NguyenandGitHub 458100cd69 Upgrade base image to version 0.10.5
Ensure libwebp is installed in the base image when imagemagick is installed so that imagemagick works with webp.
2025-10-30 14:42:18 -07:00
fogelito fea1630909 Remove redundant emailVerification 2025-10-30 18:18:09 +02:00
fogelito e40e88c322 Update emails attributes 2025-10-30 18:09:36 +02:00
copilot-swe-agent[bot]andstnguyen90 627adb43e0 Add access_type=offline and prompt=consent to Google OAuth2 login URL to enable refresh tokens
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-10-29 23:12:28 +00:00
copilot-swe-agent[bot] 9ad08694ed Initial plan 2025-10-29 23:01:33 +00:00
fogelito 037d60633b Add emailCanonical 2025-10-27 15:59:20 +02:00
fogelito 218bb4c537 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into users-add-attributes 2025-10-27 13:37:33 +02:00
Harsh MahajanandGitHub 1468231de7 Update Comment.php 2025-10-23 20:20:16 +05:30
fogelito c13a70e226 Add users attributes 2025-10-23 12:38:18 +03:00
Harsh MahajanandGitHub 32dae9ad7c Update Comment.php 2025-10-22 10:05:18 +05:30
Harsh MahajanandGitHub be5c5f9420 Merge branch 'main' into feat-SER-193-Update-facts-on-github-sites-and-functions 2025-10-22 09:53:45 +05:30
Harsh MahajanGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
64c3b7a2ff Update src/Appwrite/Vcs/Comment.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-02 23:19:34 +05:30
Harsh Mahajan 61a8f589ac port issue 2025-10-02 17:44:21 +00:00
Harsh Mahajan 92ccb6e4ea feat(vcs): expand GitHub comment tips 2025-10-02 17:43:30 +00:00
Atharva Deosthale 5963b2baf7 change request filter to versions below 1.9.0 2025-09-23 18:00:14 +05:30
Atharva Deosthale 7d4486b9e6 more fixes to tests 2025-09-19 18:42:25 +05:30
Atharva Deosthale 1a19e01d69 attempt to fix tests 2025-09-19 18:30:42 +05:30
Atharva Deosthale 2a145bc284 address reviews regarding tests 2025-09-19 18:18:44 +05:30
Atharva Deosthale 9fe3e94a66 e2e tests 2025-09-18 13:12:46 +05:30
Atharva Deosthale 8e32fb05d1 specs 2025-09-18 12:28:08 +05:30
Atharva Deosthale de8538b622 use request filters for 1.8.0 to convert into and 2025-09-18 12:27:24 +05:30
Atharva Deosthale fca2727966 add validation for type 2025-09-14 15:28:25 +05:30
Atharva Deosthale cb03bfe74c specs and sdk 2025-09-14 15:22:51 +05:30
Atharva Deosthale 2b1e3bd947 delete the new route and just do it in template route 2025-09-14 15:15:44 +05:30
Atharva Deosthale 4858317891 fix: format, lints 2025-09-13 18:02:13 +05:30
Atharva Deosthale 6e16340f16 feat: add branch deployments to appwrite 2025-09-13 17:56:19 +05:30
ArnabChatterjee20k ac604b11e3 added migration script 2025-09-01 13:13:52 +05:30
ArnabChatterjee20k f68d49c4d6 updated the migration error size attribute 2025-08-29 18:19:29 +05:30
197 changed files with 18031 additions and 5738 deletions
+301 -213
View File
@@ -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 "<details>" >> $GITHUB_STEP_SUMMARY
echo "<summary>Generated Prompt</summary>" >> $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 "</details>" >> $GITHUB_STEP_SUMMARY
echo "<details>" >> "$GITHUB_STEP_SUMMARY"
echo "<summary>Generated Prompt</summary>" >> "$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 "</details>" >> "$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(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*?--!>/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(/(?<![-\/\w])([A-Za-z][A-Za-z0-9+.-]*):(?:\/\/|(?=[^\s:]))[^\s\])}'"<>&\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(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*?--!>/g, "");
}
function convertXmlTags(s) {
const allowedTags = ["details", "summary", "code", "em", "b"];
s = s.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/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
+20 -13
View File
@@ -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
<!-- Note - this file can be customized to your needs. Replace this section directly, or add further instructions here. After editing run 'gh aw compile' -->
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.
+2 -3
View File
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
FROM appwrite/base:0.10.4 AS final
FROM appwrite/base:0.10.5 AS final
LABEL maintainer="team@appwrite.io"
@@ -28,8 +28,6 @@ RUN \
apk add boost boost-dev; \
fi
RUN apk add libwebp
WORKDIR /usr/src/code
COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor
@@ -98,6 +96,7 @@ RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/
# Enable Extensions
RUN if [ "$DEBUG" = "true" ]; then cp /usr/src/code/dev/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini; fi
RUN if [ "$DEBUG" = "true" ]; then mkdir -p /tmp/xdebug; fi
RUN if [ "$DEBUG" = "true" ]; then apk add --update --no-cache openssh-client github-cli; fi
RUN if [ "$DEBUG" = "false" ]; then rm -rf /usr/src/code/dev; fi
RUN if [ "$DEBUG" = "false" ]; then rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20230831/xdebug.so; fi
+1 -1
View File
@@ -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)
+55
View File
@@ -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' => [
[
+1 -1
View File
@@ -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,
+2 -1
View File
@@ -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'
],
],
],
+4 -4
View File
@@ -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,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -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']
],
];
+31 -2
View File
@@ -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'
],
]
],
];
+83 -7
View File
@@ -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')
-5
View File
@@ -37,7 +37,6 @@ App::get('/v1/locale')
$currencies = Config::getParam('locale-currencies');
$output = [];
$ip = $request->getIP();
$time = (60 * 60 * 24 * 45); // 45 days cache
$output['ip'] = $ip;
@@ -68,10 +67,6 @@ App::get('/v1/locale')
$output['currency'] = $currency;
}
$response
->addHeader('Cache-Control', 'public, max-age=' . $time)
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
;
$response->dynamic(new Document($output), Response::MODEL_LOCALE);
});
+71 -70
View File
@@ -49,6 +49,7 @@ use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\JSON;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -80,12 +81,12 @@ App::post('/v1/messaging/providers/mailgun')
->param('name', '', new Text(128), 'Provider name.')
->param('apiKey', '', new Text(0), 'Mailgun API Key.', true)
->param('domain', '', new Text(0), 'Mailgun Domain.', true)
->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true)
->param('isEuRegion', null, new Nullable(new Boolean()), 'Set as EU region.', true)
->param('fromName', '', new Text(128, 0), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name. Reply to name must have reply to email as well.', true)
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email. Reply to email must have reply to name as well.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -177,7 +178,7 @@ App::post('/v1/messaging/providers/sendgrid')
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name.', true)
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -259,7 +260,7 @@ App::post('/v1/messaging/providers/resend')
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name.', true)
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -366,7 +367,7 @@ App::post('/v1/messaging/providers/smtp')
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name.', true)
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -453,7 +454,7 @@ App::post('/v1/messaging/providers/msg91')
->param('templateId', '', new Text(0), 'Msg91 template ID', true)
->param('senderId', '', new Text(0), 'Msg91 sender ID.', true)
->param('authKey', '', new Text(0), 'Msg91 auth key.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -536,7 +537,7 @@ App::post('/v1/messaging/providers/telesign')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('customerId', '', new Text(0), 'Telesign customer ID.', true)
->param('apiKey', '', new Text(0), 'Telesign API key.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -620,7 +621,7 @@ App::post('/v1/messaging/providers/textmagic')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('username', '', new Text(0), 'Textmagic username.', true)
->param('apiKey', '', new Text(0), 'Textmagic apiKey.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -704,7 +705,7 @@ App::post('/v1/messaging/providers/twilio')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('accountSid', '', new Text(0), 'Twilio account secret ID.', true)
->param('authToken', '', new Text(0), 'Twilio authentication token.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -788,7 +789,7 @@ App::post('/v1/messaging/providers/vonage')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('apiKey', '', new Text(0), 'Vonage API key.', true)
->param('apiSecret', '', new Text(0), 'Vonage API secret.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -888,8 +889,8 @@ App::post('/v1/messaging/providers/fcm')
])
->param('providerId', '', new CustomId(), 'Provider 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('name', '', new Text(128), 'Provider name.')
->param('serviceAccountJSON', null, new JSON(), 'FCM service account JSON.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('serviceAccountJSON', null, new Nullable(new JSON()), 'FCM service account JSON.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -982,7 +983,7 @@ App::post('/v1/messaging/providers/apns')
->param('teamId', '', new Text(0), 'APNS team ID.', true)
->param('bundleId', '', new Text(0), 'APNS bundle ID.', true)
->param('sandbox', false, new Boolean(), 'Use APNS sandbox environment.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -1270,8 +1271,8 @@ App::patch('/v1/messaging/providers/mailgun/:providerId')
->param('name', '', new Text(128), 'Provider name.', true)
->param('apiKey', '', new Text(0), 'Mailgun API Key.', true)
->param('domain', '', new Text(0), 'Mailgun Domain.', true)
->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('isEuRegion', null, new Nullable(new Boolean()), 'Set as EU region.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128), 'Name set in the reply to field for the mail. Default value is sender name.', true)
@@ -1381,7 +1382,7 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('apiKey', '', new Text(0), 'Sendgrid API key.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
@@ -1479,7 +1480,7 @@ App::patch('/v1/messaging/providers/resend/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('apiKey', '', new Text(0), 'Resend API key.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
@@ -1597,17 +1598,17 @@ App::patch('/v1/messaging/providers/smtp/:providerId')
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('host', '', new Text(0), 'SMTP hosts. Either a single hostname or multiple semicolon-delimited hostnames. You can also specify a different port for each host such as `smtp1.example.com:25;smtp2.example.com`. You can also specify encryption type, for example: `tls://smtp1.example.com:587;ssl://smtp2.example.com:465"`. Hosts will be tried in order.', true)
->param('port', null, new Range(1, 65535), 'SMTP port.', true)
->param('port', null, new Nullable(new Range(1, 65535)), 'SMTP port.', true)
->param('username', '', new Text(0), 'Authentication username.', true)
->param('password', '', new Text(0), 'Authentication password.', true)
->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be \'ssl\' or \'tls\'', true)
->param('autoTLS', null, new Boolean(), 'Enable SMTP AutoTLS feature.', true)
->param('autoTLS', null, new Nullable(new Boolean()), 'Enable SMTP AutoTLS feature.', true)
->param('mailer', '', new Text(0), 'The value to use for the X-Mailer header.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true)
->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -1725,7 +1726,7 @@ App::patch('/v1/messaging/providers/msg91/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('templateId', '', new Text(0), 'Msg91 template ID.', true)
->param('senderId', '', new Text(0), 'Msg91 sender ID.', true)
->param('authKey', '', new Text(0), 'Msg91 auth key.', true)
@@ -1812,7 +1813,7 @@ App::patch('/v1/messaging/providers/telesign/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('customerId', '', new Text(0), 'Telesign customer ID.', true)
->param('apiKey', '', new Text(0), 'Telesign API key.', true)
->param('from', '', new Text(256), 'Sender number.', true)
@@ -1901,7 +1902,7 @@ App::patch('/v1/messaging/providers/textmagic/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('username', '', new Text(0), 'Textmagic username.', true)
->param('apiKey', '', new Text(0), 'Textmagic apiKey.', true)
->param('from', '', new Text(256), 'Sender number.', true)
@@ -1990,7 +1991,7 @@ App::patch('/v1/messaging/providers/twilio/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('accountSid', '', new Text(0), 'Twilio account secret ID.', true)
->param('authToken', '', new Text(0), 'Twilio authentication token.', true)
->param('from', '', new Text(256), 'Sender number.', true)
@@ -2079,7 +2080,7 @@ App::patch('/v1/messaging/providers/vonage/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('apiKey', '', new Text(0), 'Vonage API key.', true)
->param('apiSecret', '', new Text(0), 'Vonage API secret.', true)
->param('from', '', new Text(256), 'Sender number.', true)
@@ -2187,8 +2188,8 @@ App::patch('/v1/messaging/providers/fcm/:providerId')
])
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('serviceAccountJSON', null, new JSON(), 'FCM service account JSON.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('serviceAccountJSON', null, new Nullable(new JSON()), 'FCM service account JSON.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -2282,12 +2283,12 @@ App::patch('/v1/messaging/providers/apns/:providerId')
])
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('authKey', '', new Text(0), 'APNS authentication key.', true)
->param('authKeyId', '', new Text(0), 'APNS authentication key ID.', true)
->param('teamId', '', new Text(0), 'APNS team ID.', true)
->param('bundleId', '', new Text(0), 'APNS bundle ID.', true)
->param('sandbox', null, new Boolean(), 'Use APNS sandbox environment.', true)
->param('sandbox', null, new Nullable(new Boolean()), 'Use APNS sandbox environment.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -2676,8 +2677,8 @@ App::patch('/v1/messaging/topics/:topicId')
]
))
->param('topicId', '', new UID(), 'Topic ID.')
->param('name', null, new Text(128), 'Topic Name.', true)
->param('subscribe', null, new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with subscribe permission. By default all users are granted with any subscribe permission. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true)
->param('name', null, new Nullable(new Text(128)), 'Topic Name.', true)
->param('subscribe', null, new Nullable(new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of role strings with subscribe permission. By default all users are granted with any subscribe permission. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -3190,7 +3191,7 @@ App::post('/v1/messaging/messages/email')
->param('attachments', [], new ArrayList(new CompoundUID()), 'Array of compound ID strings of bucket IDs and file IDs to be attached to the email. They should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('draft', false, new Boolean(), 'Is message a draft', true)
->param('html', false, new Boolean(), 'Is content of type HTML', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
@@ -3363,7 +3364,7 @@ App::post('/v1/messaging/messages/sms')
->param('users', [], new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('draft', false, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
@@ -3486,7 +3487,7 @@ App::post('/v1/messaging/messages/push')
->param('topics', [], new ArrayList(new UID()), 'List of Topic IDs.', true)
->param('users', [], new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('data', null, new JSON(), 'Additional key-value pair data for push notification.', true)
->param('data', null, new Nullable(new JSON()), 'Additional key-value pair data for push notification.', true)
->param('action', '', new Text(256), 'Action for push notification.', true)
->param('image', '', new CompoundUID(), 'Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('icon', '', new Text(256), 'Icon for push notification. Available only for Android and Web Platform.', true)
@@ -3495,7 +3496,7 @@ App::post('/v1/messaging/messages/push')
->param('tag', '', new Text(256), 'Tag for push notification. Available only for Android Platform.', true)
->param('badge', -1, new Integer(), 'Badge for push notification. Available only for iOS Platform.', true)
->param('draft', false, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', false, new Boolean(), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', false, new Boolean(), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', 'high', new WhiteList(['normal', 'high']), 'Set the notification priority. "normal" will consider device state and may not deliver notifications immediately. "high" will always attempt to immediately deliver the notification.', true)
@@ -3981,17 +3982,17 @@ App::patch('/v1/messaging/messages/email/:messageId')
]
))
->param('messageId', '', new UID(), 'Message ID.')
->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true)
->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('subject', null, new Text(998), 'Email Subject.', true)
->param('content', null, new Text(64230), 'Email Content.', true)
->param('draft', null, new Boolean(), 'Is message a draft', true)
->param('html', null, new Boolean(), 'Is content of type HTML', true)
->param('cc', null, new ArrayList(new UID()), 'Array of target IDs to be added as CC.', true)
->param('bcc', null, new ArrayList(new UID()), 'Array of target IDs to be added as BCC.', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('attachments', null, new ArrayList(new CompoundUID()), 'Array of compound ID strings of bucket IDs and file IDs to be attached to the email. They should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('topics', null, new Nullable(new ArrayList(new UID())), 'List of Topic IDs.', true)
->param('users', null, new Nullable(new ArrayList(new UID())), 'List of User IDs.', true)
->param('targets', null, new Nullable(new ArrayList(new UID())), 'List of Targets IDs.', true)
->param('subject', null, new Nullable(new Text(998)), 'Email Subject.', true)
->param('content', null, new Nullable(new Text(64230)), 'Email Content.', true)
->param('draft', null, new Nullable(new Boolean()), 'Is message a draft', true)
->param('html', null, new Nullable(new Boolean()), 'Is content of type HTML', true)
->param('cc', null, new Nullable(new ArrayList(new UID())), 'Array of target IDs to be added as CC.', true)
->param('bcc', null, new Nullable(new ArrayList(new UID())), 'Array of target IDs to be added as BCC.', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('attachments', null, new Nullable(new ArrayList(new CompoundUID())), 'Array of compound ID strings of bucket IDs and file IDs to be attached to the email. They should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
@@ -4207,12 +4208,12 @@ App::patch('/v1/messaging/messages/sms/:messageId')
)
])
->param('messageId', '', new UID(), 'Message ID.')
->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true)
->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('content', null, new Text(64230), 'Email Content.', true)
->param('draft', null, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('topics', null, new Nullable(new ArrayList(new UID())), 'List of Topic IDs.', true)
->param('users', null, new Nullable(new ArrayList(new UID())), 'List of User IDs.', true)
->param('targets', null, new Nullable(new ArrayList(new UID())), 'List of Targets IDs.', true)
->param('content', null, new Nullable(new Text(64230)), 'Email Content.', true)
->param('draft', null, new Nullable(new Boolean()), 'Is message a draft', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
@@ -4369,24 +4370,24 @@ App::patch('/v1/messaging/messages/push/:messageId')
]
))
->param('messageId', '', new UID(), 'Message ID.')
->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true)
->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('title', null, new Text(256), 'Title for push notification.', true)
->param('body', null, new Text(64230), 'Body for push notification.', true)
->param('data', null, new JSON(), 'Additional Data for push notification.', true)
->param('action', null, new Text(256), 'Action for push notification.', true)
->param('image', null, new CompoundUID(), 'Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('icon', null, new Text(256), 'Icon for push notification. Available only for Android and Web platforms.', true)
->param('sound', null, new Text(256), 'Sound for push notification. Available only for Android and iOS platforms.', true)
->param('color', null, new Text(256), 'Color for push notification. Available only for Android platforms.', true)
->param('tag', null, new Text(256), 'Tag for push notification. Available only for Android platforms.', true)
->param('badge', null, new Integer(), 'Badge for push notification. Available only for iOS platforms.', true)
->param('draft', null, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', null, new Boolean(), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', null, new Boolean(), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', null, new WhiteList(['normal', 'high']), 'Set the notification priority. "normal" will consider device battery state and may send notifications later. "high" will always attempt to immediately deliver the notification.', true)
->param('topics', null, new Nullable(new ArrayList(new UID())), 'List of Topic IDs.', true)
->param('users', null, new Nullable(new ArrayList(new UID())), 'List of User IDs.', true)
->param('targets', null, new Nullable(new ArrayList(new UID())), 'List of Targets IDs.', true)
->param('title', null, new Nullable(new Text(256)), 'Title for push notification.', true)
->param('body', null, new Nullable(new Text(64230)), 'Body for push notification.', true)
->param('data', null, new Nullable(new JSON()), 'Additional Data for push notification.', true)
->param('action', null, new Nullable(new Text(256)), 'Action for push notification.', true)
->param('image', null, new Nullable(new CompoundUID()), 'Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('icon', null, new Nullable(new Text(256)), 'Icon for push notification. Available only for Android and Web platforms.', true)
->param('sound', null, new Nullable(new Text(256)), 'Sound for push notification. Available only for Android and iOS platforms.', true)
->param('color', null, new Nullable(new Text(256)), 'Color for push notification. Available only for Android platforms.', true)
->param('tag', null, new Nullable(new Text(256)), 'Tag for push notification. Available only for Android platforms.', true)
->param('badge', null, new Nullable(new Integer()), 'Badge for push notification. Available only for iOS platforms.', true)
->param('draft', null, new Nullable(new Boolean()), 'Is message a draft', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', null, new Nullable(new Boolean()), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', null, new Nullable(new Boolean()), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', null, new Nullable(new WhiteList(['normal', 'high'])), 'Set the notification priority. "normal" will consider device battery state and may send notifications later. "high" will always attempt to immediately deliver the notification.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
+4 -4
View File
@@ -468,7 +468,6 @@ App::post('/v1/migrations/csv/exports')
]
))
->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database to export.')
->param('bucketId', '', new UID(), 'Storage bucket unique ID where the exported CSV will be stored.')
->param('filename', '', new Text(255), 'The name of the file to be created for the export, excluding the .csv extension.')
->param('columns', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of attributes to export. If empty, all attributes will be exported. You can use the `*` wildcard to export all attributes from the collection.', true)
->param('queries', [], new ArrayList(new Text(0)), 'Array of query strings generated using the Query class provided by the SDK to filter documents to export. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
@@ -480,12 +479,12 @@ App::post('/v1/migrations/csv/exports')
->inject('user')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (
string $resourceId,
string $bucketId,
string $filename,
array $columns,
array $queries,
@@ -497,6 +496,7 @@ App::post('/v1/migrations/csv/exports')
Document $user,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Document $project,
Event $queueForEvents,
Migration $queueForMigrations
@@ -507,7 +507,7 @@ App::post('/v1/migrations/csv/exports')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'default'));
if ($bucket->isEmpty()) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
@@ -553,7 +553,7 @@ App::post('/v1/migrations/csv/exports')
'resourceData' => '{}',
'errors' => [],
'options' => [
'bucketId' => $bucketId,
'bucketId' => 'default', // Always use internal bucket
'filename' => $filename,
'columns' => $columns,
'queries' => $queries,
+3 -2
View File
@@ -18,6 +18,7 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DateTimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -526,8 +527,8 @@ App::put('/v1/project/variables/:variableId')
))
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Boolean(), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
->inject('project')
->inject('response')
->inject('dbForProject')
+8 -7
View File
@@ -46,6 +46,7 @@ use Utopia\Validator\Boolean;
use Utopia\Validator\Hostname;
use Utopia\Validator\Integer;
use Utopia\Validator\Multiple;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
@@ -678,9 +679,9 @@ App::patch('/v1/projects/:projectId/oauth2')
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'Provider Name')
->param('appId', null, new Text(256), 'Provider app ID. Max length: 256 chars.', true)
->param('secret', null, new text(512), 'Provider secret key. Max length: 512 chars.', true)
->param('enabled', null, new Boolean(), 'Provider status. Set to \'false\' to disable new session creation.', true)
->param('appId', null, new Nullable(new Text(256)), 'Provider app ID. Max length: 256 chars.', true)
->param('secret', null, new Nullable(new text(512)), 'Provider secret key. Max length: 512 chars.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Provider status. Set to \'false\' to disable new session creation.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $provider, ?string $appId, ?string $secret, ?bool $enabled, Response $response, Database $dbForPlatform) {
@@ -1476,8 +1477,8 @@ App::post('/v1/projects/:projectId/keys')
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
@@ -1615,8 +1616,8 @@ App::put('/v1/projects/:projectId/keys/:keyId')
->param('projectId', '', new UID(), 'Project unique ID.')
->param('keyId', '', new UID(), 'Key unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
+12 -9
View File
@@ -50,6 +50,7 @@ use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\HexColor;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -77,7 +78,7 @@ App::post('/v1/storage/buckets')
))
->param('bucketId', '', 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.')
->param('name', '', new Text(128), 'Bucket name')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of permission strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('fileSecurity', false, new Boolean(true), 'Enables configuring permissions for individual file. A user needs one of file or bucket level permissions to access a file. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
@@ -290,7 +291,7 @@ App::put('/v1/storage/buckets/:bucketId')
))
->param('bucketId', '', new UID(), 'Bucket unique ID.')
->param('name', null, new Text(128), 'Bucket name', false)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('fileSecurity', false, new Boolean(true), 'Enables configuring permissions for individual file. A user needs one of file or bucket level permissions to access a file. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
@@ -418,7 +419,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
->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 CustomId(), 'File 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('file', [], new File(), 'Binary file. Appwrite SDKs provide helpers to handle file input. [Learn about file input](https://appwrite.io/docs/products/storage/upload-download#input-file).', skipValidation: true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permission strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE])), 'An array of permission strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->inject('request')
->inject('response')
->inject('dbForProject')
@@ -1477,12 +1478,11 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
->inject('response')
->inject('request')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('mode')
->inject('deviceForFiles')
->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Document $project, string $mode, Device $deviceForFiles) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Database $dbForPlatform, Document $project, string $mode, Device $deviceForFiles) {
$decoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
try {
@@ -1499,15 +1499,18 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$isInternal = $decoded['internal'] ?? false;
$dbForProject = $isInternal ? $dbForPlatform : $dbForProject;
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
@@ -1645,8 +1648,8 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
))
->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('name', null, new Text(255), 'Name of the file', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permission string. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('name', null, new Nullable(new Text(255)), 'Name of the file', true)
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE])), 'An array of permission string. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->inject('response')
->inject('dbForProject')
->inject('user')
+49 -34
View File
@@ -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);
}
+37 -13
View File
@@ -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,12 +49,14 @@ 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;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -97,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,
@@ -124,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') {
@@ -208,8 +221,8 @@ 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 Email(), 'User email.', true)
->param('phone', null, new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', 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)
->inject('response')
@@ -243,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')
@@ -278,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')
@@ -313,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')
@@ -348,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)
@@ -390,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')
@@ -425,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.')
@@ -473,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.')
@@ -527,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);
}
@@ -1402,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')
@@ -1437,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 {
@@ -1700,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);
}
+4
View File
@@ -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();
+3 -1
View File
@@ -234,7 +234,9 @@ App::init()
->inject('apiKey')
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey) {
$route = $utopia->getRoute();
if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted' && str_starts_with($route->getPath(), '/v1/backups')) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Database Backups are available on Appwrite Cloud');
}
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
+3
View File
@@ -138,6 +138,7 @@ const DELETE_TYPE_TOPIC = 'topic';
const DELETE_TYPE_TARGET = 'target';
const DELETE_TYPE_EXPIRED_TARGETS = 'invalid_targets';
const DELETE_TYPE_SESSION_TARGETS = 'session_targets';
const DELETE_TYPE_CSV_EXPORTS = 'csv_exports';
const DELETE_TYPE_MAINTENANCE = 'maintenance';
// Message types
@@ -271,6 +272,8 @@ const METRIC_SITES_ID_REQUESTS = 'sites.{siteInternalId}.requests';
const METRIC_SITES_ID_INBOUND = 'sites.{siteInternalId}.inbound';
const METRIC_SITES_ID_OUTBOUND = 'sites.{siteInternalId}.outbound';
const METRIC_AVATARS_SCREENSHOTS_GENERATED = 'avatars.screenshotsGenerated';
const METRIC_FUNCTIONS_RUNTIME = 'functions.runtimes.{runtime}';
const METRIC_SITES_FRAMEWORK = 'sites.frameworks.{framework}';
// Resource types
const RESOURCE_TYPE_PROJECTS = 'projects';
Generated
+83 -77
View File
@@ -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",
+7
View File
@@ -0,0 +1,7 @@
services:
appwrite:
# volumes:
# - ~/.ssh:/root/.ssh
environment:
- GH_TOKEN=
- GIT_EMAIL=
@@ -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://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
.setProject("<YOUR_PROJECT_ID>"); // 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)
"<USER_AGENT>", // userAgent (optional)
false, // fullpage (optional)
"<LOCALE>", // 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());
})
);
@@ -0,0 +1,32 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Avatars
val client = Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
.setProject("<YOUR_PROJECT_ID>") // 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 = "<USER_AGENT>", // (optional)
fullpage = false, // (optional)
locale = "<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)
)
@@ -0,0 +1,32 @@
import Appwrite
import AppwriteEnums
let client = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
.setProject("<YOUR_PROJECT_ID>") // 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: "<USER_AGENT>", // optional
fullpage: false, // optional
locale: "<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
)
@@ -0,0 +1,65 @@
import 'package:appwrite/appwrite.dart';
Client client = Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // 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: '<USER_AGENT>', // optional
fullpage: false, // optional
locale: '<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:'<USER_AGENT>' , // optional
fullpage:false , // optional
locale:'<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();
}
);
@@ -0,0 +1,32 @@
import { Client, Avatars, , , } from "react-native-appwrite";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // 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: '<USER_AGENT>', // optional
fullpage: false, // optional
locale: '<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);
@@ -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: <YOUR_PROJECT_ID>
X-Appwrite-Session:
X-Appwrite-JWT: <YOUR_JWT>
@@ -0,0 +1,32 @@
import { Client, Avatars, , , } from "appwrite";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // 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: '<USER_AGENT>', // optional
fullpage: false, // optional
locale: '<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);
@@ -1,4 +1,3 @@
appwrite migrations create-csv-export \
--resource-id <ID1:ID2> \
--bucket-id <BUCKET_ID> \
--filename <FILENAME>
@@ -0,0 +1,32 @@
import { Client, Avatars, , , } from "@appwrite.io/console";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // 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: '<USER_AGENT>', // optional
fullpage: false, // optional
locale: '<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);
@@ -8,7 +8,6 @@ const migrations = new Migrations(client);
const result = await migrations.createCSVExport({
resourceId: '<ID1:ID2>',
bucketId: '<BUCKET_ID>',
filename: '<FILENAME>',
columns: [], // optional
queries: [], // optional
@@ -0,0 +1,31 @@
import 'package:dart_appwrite/dart_appwrite.dart';
Client client = Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>') // 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: '<USER_AGENT>', // (optional)
fullpage: false, // (optional)
locale: '<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)
);
@@ -0,0 +1,34 @@
using Appwrite;
using Appwrite.Enums;
using Appwrite.Models;
using Appwrite.Services;
Client client = new Client()
.SetEndPoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("<YOUR_PROJECT_ID>") // 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: "<USER_AGENT>", // optional
fullpage: false, // optional
locale: "<LOCALE>", // optional
timezone: .AfricaAbidjan, // optional
latitude: -90, // optional
longitude: -180, // optional
accuracy: 0, // optional
touch: false, // optional
permissions: new List<string>(), // optional
sleep: 0, // optional
width: 0, // optional
height: 0, // optional
quality: -1, // optional
output: .Jpg // optional
);
@@ -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://<REGION>.cloud.appwrite.io/v1")
client.WithProject("<YOUR_PROJECT_ID>")
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("<USER_AGENT>"),
avatars.WithGetScreenshotFullpage(false),
avatars.WithGetScreenshotLocale("<LOCALE>"),
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"),
)
@@ -0,0 +1,42 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Avatars;
Client client = new Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
.setProject("<YOUR_PROJECT_ID>") // 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)
"<USER_AGENT>", // userAgent (optional)
false, // fullpage (optional)
"<LOCALE>", // 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);
})
);
@@ -0,0 +1,33 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Avatars
val client = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
.setProject("<YOUR_PROJECT_ID>") // 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 = "<USER_AGENT>", // optional
fullpage = false, // optional
locale = "<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
)
@@ -0,0 +1,31 @@
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>') // 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: '<USER_AGENT>', // optional
fullpage: false, // optional
locale: '<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
});
@@ -0,0 +1,37 @@
<?php
use Appwrite\Client;
use Appwrite\Services\Avatars;
use Appwrite\Enums\Theme;
use Appwrite\Enums\Timezone;
use Appwrite\Enums\Output;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
->setProject('<YOUR_PROJECT_ID>') // 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: '<USER_AGENT>', // optional
fullpage: false, // optional
locale: '<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
);
@@ -3,6 +3,7 @@
use Appwrite\Client;
use Appwrite\Services\Databases;
use Appwrite\Enums\RelationshipType;
use Appwrite\Enums\RelationMutate;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Databases;
use Appwrite\Enums\RelationMutate;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Functions;
use Appwrite\Enums\ExecutionMethod;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,7 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Functions;
use Appwrite\Enums\;
use Appwrite\Enums\Runtime;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -14,7 +14,7 @@ $functions = new Functions($client);
$result = $functions->create(
functionId: '<FUNCTION_ID>',
name: '<NAME>',
runtime: ::NODE145(),
runtime: Runtime::NODE145(),
execute: ["any"], // optional
events: [], // optional
schedule: '', // optional
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Functions;
use Appwrite\Enums\DeploymentDownloadType;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Functions;
use Appwrite\Enums\Runtime;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -13,7 +14,7 @@ $functions = new Functions($client);
$result = $functions->update(
functionId: '<FUNCTION_ID>',
name: '<NAME>',
runtime: ::NODE145(), // optional
runtime: Runtime::NODE145(), // optional
execute: ["any"], // optional
events: [], // optional
schedule: '', // optional
@@ -2,7 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Health;
use Appwrite\Enums\;
use Appwrite\Enums\Name;
$client = (new Client())
->setEndpoint('https://<REGION>.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
);
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Messaging;
use Appwrite\Enums\MessagePriority;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Messaging;
use Appwrite\Enums\SmtpEncryption;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Messaging;
use Appwrite\Enums\MessagePriority;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Messaging;
use Appwrite\Enums\SmtpEncryption;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -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://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -15,15 +16,15 @@ $sites = new Sites($client);
$result = $sites->create(
siteId: '<SITE_ID>',
name: '<NAME>',
framework: ::ANALOG(),
buildRuntime: ::NODE145(),
framework: Framework::ANALOG(),
buildRuntime: BuildRuntime::NODE145(),
enabled: false, // optional
logging: false, // optional
timeout: 1, // optional
installCommand: '<INSTALL_COMMAND>', // optional
buildCommand: '<BUILD_COMMAND>', // optional
outputDirectory: '<OUTPUT_DIRECTORY>', // optional
adapter: ::STATIC(), // optional
adapter: Adapter::STATIC(), // optional
installationId: '<INSTALLATION_ID>', // optional
fallbackFile: '<FALLBACK_FILE>', // optional
providerRepositoryId: '<PROVIDER_REPOSITORY_ID>', // optional
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Sites;
use Appwrite\Enums\DeploymentDownloadType;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -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://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -14,15 +16,15 @@ $sites = new Sites($client);
$result = $sites->update(
siteId: '<SITE_ID>',
name: '<NAME>',
framework: ::ANALOG(),
framework: Framework::ANALOG(),
enabled: false, // optional
logging: false, // optional
timeout: 1, // optional
installCommand: '<INSTALL_COMMAND>', // optional
buildCommand: '<BUILD_COMMAND>', // optional
outputDirectory: '<OUTPUT_DIRECTORY>', // optional
buildRuntime: ::NODE145(), // optional
adapter: ::STATIC(), // optional
buildRuntime: BuildRuntime::NODE145(), // optional
adapter: Adapter::STATIC(), // optional
fallbackFile: '<FALLBACK_FILE>', // optional
installationId: '<INSTALLATION_ID>', // optional
providerRepositoryId: '<PROVIDER_REPOSITORY_ID>', // optional
@@ -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
);
@@ -2,6 +2,8 @@
use Appwrite\Client;
use Appwrite\Services\Storage;
use Appwrite\Enums\ImageGravity;
use Appwrite\Enums\ImageFormat;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -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
);
@@ -3,6 +3,7 @@
use Appwrite\Client;
use Appwrite\Services\TablesDB;
use Appwrite\Enums\RelationshipType;
use Appwrite\Enums\RelationMutate;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\TablesDB;
use Appwrite\Enums\RelationMutate;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Users;
use Appwrite\Enums\PasswordHash;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -0,0 +1,32 @@
from appwrite.client import Client
from appwrite.services.avatars import Avatars
client = Client()
client.set_endpoint('https://<REGION>.cloud.appwrite.io/v1') # Your API Endpoint
client.set_project('<YOUR_PROJECT_ID>') # 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 = '<USER_AGENT>', # optional
fullpage = False, # optional
locale = '<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
)
@@ -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: <YOUR_PROJECT_ID>
X-Appwrite-Session:
X-Appwrite-Key: <YOUR_API_KEY>
X-Appwrite-JWT: <YOUR_JWT>
@@ -0,0 +1,33 @@
require 'appwrite'
include Appwrite
client = Client.new
.set_endpoint('https://<REGION>.cloud.appwrite.io/v1') # Your API Endpoint
.set_project('<YOUR_PROJECT_ID>') # 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: '<USER_AGENT>', # optional
fullpage: false, # optional
locale: '<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
)
@@ -0,0 +1,33 @@
import Appwrite
import AppwriteEnums
let client = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
.setProject("<YOUR_PROJECT_ID>") // 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: "<USER_AGENT>", // optional
fullpage: false, // optional
locale: "<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
)
@@ -1 +1 @@
Export documents to a CSV file from your Appwrite database. This endpoint allows you to export documents to a CSV file stored in an Appwrite Storage bucket.
Export documents to a CSV file from your Appwrite database. This endpoint allows you to export documents to a CSV file stored in a secure internal bucket. You'll receive an email with a download link when the export is complete.
+4
View File
@@ -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
+7
View File
@@ -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
+4
View File
@@ -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
+10
View File
@@ -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
+12 -19
View File
@@ -26,34 +26,27 @@ Before releasing SDKs, you need to:
To enable SDK releases via GitHub, you need to mount SSH keys and configure GitHub authentication in your Docker environment.
#### Update Dockerfile
#### Update docker-compose.override.yml
Add the following configuration to your `Dockerfile`:
```dockerfile
ARG GH_TOKEN
ENV GH_TOKEN=your_github_token_here
RUN git config --global user.email "your-email@example.com"
RUN apk add --update --no-cache openssh-client github-cli
```
Replace:
- `your_github_token_here` with your GitHub personal access token (with appropriate permissions)
- `your-email@example.com` with your Git email address
#### Update docker-compose.yml
Add the SSH key volume mount to the `appwrite` service in `docker-compose.yml`:
Update `docker-compose.override.yml` to mount SSH keys and set environment variables for the `appwrite` service:
```yaml
services:
appwrite:
volumes:
- ~/.ssh:/root/.ssh
# ... other volumes
environment:
- GH_TOKEN=your_github_token_here
- GIT_EMAIL=your-email@example.com
```
This mounts your SSH keys from the host machine, allowing the container to authenticate with GitHub.
Uncomment the volumes section.
Replace:
- `your_github_token_here` with your GitHub personal access token (with appropriate permissions)
- `your-email@example.com` with your Git email address
This mounts your SSH keys from the host machine and sets the GitHub token and email as environment variables, allowing the container to authenticate with GitHub. The git configuration is handled automatically at runtime.
### Updating Specs
+1
View File
@@ -31,6 +31,7 @@
<directory>./tests/e2e/Services/Locale</directory>
<directory>./tests/e2e/Services/Projects</directory>
<directory>./tests/e2e/Services/Storage</directory>
<directory>./tests/e2e/Services/Tokens</directory>
<directory>./tests/e2e/Services/Webhooks</directory>
<directory>./tests/e2e/Services/Messaging</directory>
<directory>./tests/e2e/Services/Migrations</directory>
Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

+3 -1
View File
@@ -53,7 +53,9 @@ class Google extends OAuth2
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes()),
'state' => \json_encode($this->state),
'response_type' => 'code'
'response_type' => 'code',
'access_type' => 'offline',
'prompt' => 'consent'
]);
}
+50
View File
@@ -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');
}
}
@@ -2,9 +2,13 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases;
use Utopia\Platform\Action as UtopiaAction;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action as AppwriteAction;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Operator;
class Action extends UtopiaAction
class Action extends AppwriteAction
{
private string $context = 'legacy';
@@ -13,11 +17,81 @@ class Action extends UtopiaAction
return $this->context;
}
public function setHttpPath(string $path): UtopiaAction
public function setHttpPath(string $path): AppwriteAction
{
if (\str_contains($path, '/tablesdb')) {
$this->context = 'tablesdb';
}
return parent::setHttpPath($path);
}
/**
* Parse operator strings in data array and convert them to Operator objects.
*
* @param array $data The data array that may contain operator JSON strings or arrays
* @param Document $collection The collection document to check for relationship attributes
* @return array The data array with operators converted to Operator objects
* @throws Exception If an operator string is invalid
*/
protected function parseOperators(array $data, Document $collection): array
{
$relationshipKeys = [];
foreach ($collection->getAttribute('attributes', []) as $attribute) {
if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) {
$relationshipKeys[$attribute->getAttribute('key')] = true;
}
}
foreach ($data as $key => $value) {
if (!\is_string($key)) {
if (\is_array($value)) {
$data[$key] = $this->parseOperators($value, $collection);
}
continue;
}
if (\str_starts_with($key, '$')) {
continue;
}
if (isset($relationshipKeys[$key])) {
continue;
}
// Handle operator as JSON string (from API requests)
if (\is_string($value)) {
$decoded = \json_decode($value, true);
if (
\is_array($decoded) &&
isset($decoded['method']) &&
\is_string($decoded['method']) &&
Operator::isMethod($decoded['method'])
) {
try {
$data[$key] = Operator::parse($value);
} catch (\Exception $e) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid operator for attribute "' . $key . '": ' . $e->getMessage());
}
}
}
// Handle operator as array (from transaction logs after serialization)
elseif (
\is_array($value) &&
isset($value['method']) &&
\is_string($value['method']) &&
Operator::isMethod($value['method'])
) {
try {
$data[$key] = Operator::parseOperator($value);
} catch (\Exception $e) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid operator for attribute "' . $key . '": ' . $e->getMessage());
}
} elseif (\is_array($value)) {
$data[$key] = $this->parseOperators($value, $collection);
}
}
return $data;
}
}
@@ -16,6 +16,7 @@ use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
class Create extends Action
{
@@ -62,7 +63,7 @@ class Create extends Action
->param('collectionId', '', new UID(), 'Collection ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Boolean(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('default', null, new Nullable(new Boolean()), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
@@ -64,7 +64,7 @@ class Update extends Action
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Nullable(new Boolean()), 'Default value for attribute when not provided. Cannot be set when attribute is required.')
->param('newKey', null, new Key(), 'New attribute key.', true)
->param('newKey', null, new Nullable(new Key()), 'New attribute key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
@@ -17,6 +17,7 @@ use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
class Create extends Action
{
@@ -63,7 +64,7 @@ class Create extends Action
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#createCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, fn (Database $dbForProject) => new DatetimeValidator($dbForProject->getAdapter()->getMinDateTime(), $dbForProject->getAdapter()->getMaxDateTime()), 'Default value for the attribute in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.', true, ['dbForProject'])
->param('default', null, fn (Database $dbForProject) => new Nullable(new DatetimeValidator($dbForProject->getAdapter()->getMinDateTime(), $dbForProject->getAdapter()->getMaxDateTime())), 'Default value for the attribute in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.', true, ['dbForProject'])
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
@@ -65,7 +65,7 @@ class Update extends Action
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, fn (Database $dbForProject) => new Nullable(new DatetimeValidator($dbForProject->getAdapter()->getMinDateTime(), $dbForProject->getAdapter()->getMaxDateTime())), 'Default value for attribute when not provided. Cannot be set when attribute is required.', injections: ['dbForProject'])
->param('newKey', null, new Key(), 'New attribute key.', true)
->param('newKey', null, new Nullable(new Key()), 'New attribute key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
@@ -17,6 +17,7 @@ use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
class Create extends Action
{
@@ -63,7 +64,7 @@ class Create extends Action
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Email(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('default', null, new Nullable(new Email()), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
@@ -65,7 +65,7 @@ class Update extends Action
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Nullable(new Email()), 'Default value for attribute when not provided. Cannot be set when attribute is required.')
->param('newKey', null, new Key(), 'New Attribute Key.', true)
->param('newKey', null, new Nullable(new Key()), 'New Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
@@ -18,6 +18,7 @@ use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Create extends Action
@@ -66,7 +67,7 @@ class Create extends Action
->param('key', '', new Key(), 'Attribute Key.')
->param('elements', [], new ArrayList(new Text(Database::LENGTH_KEY), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of enum values.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Text(0), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('default', null, new Nullable(new Text(0)), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
@@ -67,7 +67,7 @@ class Update extends Action
->param('elements', null, new ArrayList(new Text(Database::LENGTH_KEY), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Updated list of enum values.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Nullable(new Text(0)), 'Default value for attribute when not provided. Cannot be set when attribute is required.')
->param('newKey', null, new Key(), 'New Attribute Key.', true)
->param('newKey', null, new Nullable(new Key()), 'New Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
@@ -18,6 +18,7 @@ use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\FloatValidator;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
class Create extends Action
@@ -65,9 +66,9 @@ class Create extends Action
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('min', null, new FloatValidator(), 'Minimum value.', true)
->param('max', null, new FloatValidator(), 'Maximum value.', true)
->param('default', null, new FloatValidator(), 'Default value. Cannot be set when required.', true)
->param('min', null, new Nullable(new FloatValidator()), 'Minimum value.', true)
->param('max', null, new Nullable(new FloatValidator()), 'Maximum value.', true)
->param('default', null, new Nullable(new FloatValidator()), 'Default value. Cannot be set when required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')

Some files were not shown because too many files have changed in this diff Show More