Compare commits

...
Author SHA1 Message Date
57a811eb5d chore: update utopia-php/database to stable 5.* and run composer update
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/045dbae8-cb3c-42ae-bfa0-144d0680ba26

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-04-29 07:29:48 +00:00
e985d8724b Merge remote-tracking branch 'origin/1.9.x' into unique-exception
# Conflicts:
#	composer.json
#	composer.lock
#	tests/e2e/Services/Databases/DatabasesBase.php

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-04-29 06:49:59 +00:00
ArnabChatterjee20kandGitHub dae9cbcf45 Merge pull request #12070 from appwrite/realtime-action-channels
Realtime action channels
2026-04-29 10:49:13 +05:30
Harsh MahajanandGitHub 547709a1d8 Merge pull request #12167 from appwrite/feat/impersonation-query-params
feat: add query param fallback for impersonation headers
2026-04-28 19:51:23 +05:30
harsh mahajan 2a357511ea fix: use unique emails and phone in query param impersonation test 2026-04-28 19:17:25 +05:30
Harsh MahajanandGitHub 67d24d3ef1 Merge branch '1.9.x' into feat/impersonation-query-params 2026-04-28 19:11:14 +05:30
harsh mahajan 87ed7c3817 feat: add query param fallback for all impersonation params and simplify tests 2026-04-28 19:10:55 +05:30
Matej BačoandGitHub 3d3f5934c6 Merge pull request #11993 from appwrite/feat-public-oauth2-endpoints
Feat: Public project OAuth2 configuration API
2026-04-28 12:41:50 +02:00
harsh mahajan f0cbfbbbe4 fix: use assertEmpty for impersonatorUserId to match response model 2026-04-28 14:31:49 +05:30
Matej Bačo cb4cff120b Add Keycloak oauth support 2026-04-28 10:54:13 +02:00
Matej Bačo 49e6a38e7f Add fusionauth oauth 2026-04-28 10:43:16 +02:00
Matej Bačo dfa3ae5274 Fix tests 2026-04-28 10:19:36 +02:00
Matej Bačo 543765a22a Improve copy 2026-04-28 10:15:45 +02:00
Matej Bačo e2bb9a9161 Simplify oauth endpoints 2026-04-28 10:08:39 +02:00
harsh mahajan bda823ac0e chore: format 2026-04-28 13:38:00 +05:30
harsh mahajan 3dd5a51ba4 style: fix method argument spacing (Pint PSR-12) 2026-04-28 13:34:01 +05:30
harsh mahajan 5afc8f462d fix: allow same-site in CSRF guard to support Console on subdomains 2026-04-28 13:26:13 +05:30
harsh mahajan ed0c7b4e12 test: add CSRF attack prevention test for impersonateUserId query param 2026-04-28 13:24:15 +05:30
Matej Bačo d25707346f Add console oauth endpoint 2026-04-28 09:47:27 +02:00
harsh mahajan a3f6cf4645 fix: restrict CSRF guard to same-origin only, drop same-site 2026-04-28 13:00:18 +05:30
harsh mahajan 9a175c5098 test: add E2E tests for impersonateUserId query param and CSRF guards 2026-04-28 12:56:17 +05:30
harsh mahajan 5465be6301 fix: make CSRF guard fail-closed by requiring explicit same-origin Sec-Fetch-Site 2026-04-28 12:27:57 +05:30
harsh mahajan 46a457bfa3 fix: block impersonateUserId query param on cross-site requests to prevent CSRF 2026-04-28 12:10:51 +05:30
harsh mahajan 4c989f99c3 fix: cast impersonateUserId query param to string to prevent array injection 2026-04-28 12:05:02 +05:30
harsh mahajan 8f1d73a6cb chore: clarify intentional header-only restriction for email/phone impersonation 2026-04-28 12:02:00 +05:30
harsh mahajan 01b5fa8ecb fix: restrict impersonation query param fallback to userId only
Remove query param fallback for impersonateEmail and impersonatePhone
to avoid PII exposure in server logs, browser history, and Referer
headers. Only impersonateUserId (an opaque internal ID) is safe to
pass via URL query param.
2026-04-28 11:58:25 +05:30
harsh mahajan d73b7a70d8 feat: add query param fallback for impersonation headers
Allow impersonation to be specified via URL query params
(?impersonateUserId, ?impersonateEmail, ?impersonatePhone) as a
fallback to the existing headers, enabling Console to embed
impersonation in direct file/image URLs where headers cannot be set.
2026-04-28 11:44:39 +05:30
Damodar LohaniandGitHub cefd063c55 Merge pull request #12165 from appwrite/fix/CLO-4280-getheader-string-coerce
fix: coerce non-string header values in Request::getHeader
2026-04-28 10:43:40 +05:45
Damodar LohaniandGitHub c924cbcc59 Merge pull request #12166 from appwrite/fix/CLO-4279-favicon-empty-body
fix: guard DOMDocument::loadHTML against empty body in favicon endpoint
2026-04-28 10:32:32 +05:45
Damodar LohaniGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
81321e82d1 Update src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-28 10:05:01 +05:45
Damodar Lohani 30a511692b test: add unit coverage for Request::getHeader non-string coercion
Refs CLO-4280
2026-04-28 04:15:00 +00:00
Damodar Lohani 9637409831 fix: coerce non-string header values in Request::getHeader
Closes CLO-4280
2026-04-28 03:54:35 +00:00
Damodar Lohani c4f6b11706 fix: guard DOMDocument::loadHTML against empty body in favicon endpoint
Closes CLO-4279
2026-04-28 03:54:34 +00:00
Matej Bačo ad4178aa42 Fix missing lib params for domain 2026-04-27 18:33:30 +02:00
Matej Bačo 1f16b0d9e7 Fix failing startup 2026-04-27 18:21:21 +02:00
Matej Bačo 015aee087a Fix write only security 2026-04-27 18:04:22 +02:00
Matej Bačo 50d86c5b5d Update ci.yml 2026-04-27 17:45:52 +02:00
Matej Bačo 3d43530225 Fix failing test 2026-04-27 17:41:13 +02:00
Matej Bačo d0d536a2dd Improve test coverage 2026-04-27 17:40:49 +02:00
Matej Bačo 4b620bb31a Improve test coverage 2026-04-27 17:27:23 +02:00
Matej Bačo ca7f36a9b8 Fix bugs by improving tests 2026-04-27 17:17:57 +02:00
Matej Bačo ec3c7f1ad6 Fix failing oauth tests 2026-04-27 17:02:53 +02:00
Matej Bačo ecba11eba5 Brin back removed tests 2026-04-27 16:54:53 +02:00
Matej Bačo 7a96b024b3 Fix tests 2026-04-27 16:51:01 +02:00
Matej Bačo 4ba413fcc0 Fix bugs when implementing tests 2026-04-27 16:50:14 +02:00
Matej Bačo af95e71244 Add OAUth update tests 2026-04-27 16:02:19 +02:00
Matej Bačo ee1eea5c0c oauth tests setup 2026-04-27 15:51:54 +02:00
Matej Bačo b28b851bb2 microsoft oauth endpoint 2026-04-27 15:49:44 +02:00
ArnabChatterjee20k 70b9c60e2c test(Messaging): validate that bare functions channel is not emitted in published channels 2026-04-27 18:46:04 +05:30
ArnabChatterjee20k cb8640b56f feat(Realtime): enhance channel management for user authentication and account actions 2026-04-27 18:24:52 +05:30
Matej Bačo a781325679 Add oauth read operations 2026-04-27 14:47:47 +02:00
ArnabChatterjee20k 3f12062259 updated 2026-04-27 17:54:48 +05:30
Matej Bačo 2e57500d7e WIP: Read endpoints for oauth 2026-04-27 14:16:43 +02:00
Matej Bačo a1a88ae57e Make oauth secret write only 2026-04-27 14:09:24 +02:00
ArnabChatterjee20k 9553f8a9f8 refactor(MessagingTest): update method visibility and naming conventions for consistency 2026-04-27 17:35:56 +05:30
Matej Bačo 15f94d99ca Add Kick OAuth adapter 2026-04-27 14:02:30 +02:00
ArnabChatterjee20k ca105ff9bc feat(Realtime): implement rebindAccountChannels method for userId changes and add corresponding tests 2026-04-27 17:31:31 +05:30
ArnabChatterjee20k 7e3114d733 linting 2026-04-27 17:26:27 +05:30
ArnabChatterjee20k ef4b9c4934 updated 2026-04-27 17:16:17 +05:30
Matej Bačo 2e960b90df Fix unused env variable 2026-04-27 13:38:26 +02:00
ArnabChatterjee20k 1928605bd9 linting 2026-04-27 16:42:45 +05:30
ArnabChatterjee20k 340ce9d56b Add tests for channel conversion and event handling in Messaging
- Implement `test_convert_channels_rewrites_account_action_suffixes` to ensure
  that account action suffixes are correctly rewritten to user-scoped channels.
- Add `test_convert_channels_drops_account_actions_for_guest` to verify that
  account actions are dropped for guests without a user ID.
- Introduce `test_from_payload_does_not_suffix_account_for_nested_user_events`
  to confirm that nested user events do not leak action suffixes onto account channels.
2026-04-27 16:40:15 +05:30
ArnabChatterjee20k e6d5c216eb refactor(Realtime): update action extraction logic and enhance test method naming conventions 2026-04-27 16:06:00 +05:30
ArnabChatterjee20k d25ccb784d refactor(Realtime): remove SUPPORTED_ACTIONS constant and simplify action extraction logic 2026-04-27 15:59:34 +05:30
Matej Bačo 8ce7aa2abe Fix crashing http 2026-04-27 12:27:52 +02:00
ArnabChatterjee20k 78715e4a1a refactor(tests): rename test methods to snake_case and update assertions for action channels
- Changed test method names from camelCase to snake_case for consistency.
- Updated assertions to ensure action channels are correctly emitted and filtered.
- Improved readability and maintainability of the test suite by restructuring test cases.
2026-04-27 15:46:02 +05:30
ArnabChatterjee20k df57ee2a32 added unit test 2026-04-27 13:43:23 +05:30
ArnabChatterjee20k 6d4a66fbb3 Enhance Realtime adapter to support action-channel awareness in subscriber checks and add corresponding tests 2026-04-27 13:30:18 +05:30
ArnabChatterjee20k 3aee54747c Enhance Realtime adapter to support delete action and add corresponding tests 2026-04-27 13:15:04 +05:30
ArnabChatterjee20k d2423a5bb5 added tests 2026-04-27 12:57:35 +05:30
ArnabChatterjee20k c0c053ff20 Enhance Realtime adapter with action channel support and tests
- Introduced ACTION_ALL and SUPPORTED_ACTIONS constants for better action handling.
- Updated channel subscription logic to support action suffixes.
- Added tests for action channel parsing and filtering in MessagingTest.
2026-04-27 12:52:52 +05:30
Matej Bačo e4bfb38a57 add okta provider 2026-04-26 11:14:50 +02:00
Matej Bačo 0a7b7de197 Revert changes - default works as fallback for optional serverID 2026-04-26 10:59:29 +02:00
Matej Bačo 51c0767be2 Make okta server ID optional 2026-04-26 10:56:41 +02:00
Matej Bačo d25dac7d60 Manual quality improvmenets 2026-04-26 10:29:41 +02:00
Matej BačoandGitHub 1f18e16310 Merge branch '1.9.x' into feat-public-oauth2-endpoints 2026-04-25 12:45:34 +02:00
Matej Bačo d0f6daa67a Fix integration test 2026-04-25 12:05:35 +02:00
Matej Bačo 184399023c Add github integration test 2026-04-25 11:58:09 +02:00
Matej Bačo a588a62277 Prepare env for cicd integration with github oauth 2026-04-25 11:57:40 +02:00
Matej Bačo ffd0dbd406 Add OIDC endpoint 2026-04-25 10:20:00 +02:00
Matej Bačo 8200d079c6 Simplify specs 2026-04-24 16:37:27 +02:00
Matej Bačo d9d87f813f apple oauth endpoints 2026-04-24 16:31:21 +02:00
Matej Bačo db7acd4b8b More OAuth endpoints 2026-04-24 15:02:36 +02:00
Matej Bačo a62ca8612d More OAuth endpoints 2026-04-24 14:31:38 +02:00
Matej Bačo 975da667f5 Remove leftover 2026-04-24 14:23:19 +02:00
Matej Bačo 6cfb12c48b Improve OAuth SDK quality 2026-04-24 14:23:04 +02:00
Matej Bačo 8cdcd379c8 Add more oauth endpoints 2026-04-24 14:15:34 +02:00
ArnabChatterjee20kandGitHub 1b8123bf62 Merge pull request #11992 from appwrite/realtime-logs
added missing include for exporter in the realtime
2026-04-24 16:45:28 +05:30
ArnabChatterjee20kandGitHub 1ca75c73df Merge branch '1.9.x' into realtime-logs 2026-04-24 16:35:25 +05:30
Matej Bačo fe08978851 More OAuth provider endpoints 2026-04-24 12:58:32 +02:00
ArnabChatterjee20k 0633662695 removed dispatch experiment 2026-04-24 16:22:57 +05:30
ArnabChatterjee20k 89819db775 added exporter 2026-04-24 16:12:42 +05:30
Matej Bačo faf09ed7c5 Abstrated oauth response model 2026-04-24 12:38:12 +02:00
Matej Bačo c097d9fcdd Dropbox adapter 2026-04-24 12:20:48 +02:00
Matej Bačo dac184b281 abstract oauth adapters 2026-04-24 12:06:58 +02:00
Matej Bačo 335b1c2f6c Figma OAuth endpoint 2026-04-24 11:45:59 +02:00
Matej Bačo 5fbe6cba79 Improve github samples 2026-04-24 11:39:14 +02:00
Matej Bačo 36435d940d Add Discord OAuth endpoint 2026-04-24 11:35:30 +02:00
Matej Bačo 93f7a0d902 GitHub oauth endpoint 2026-04-24 11:17:18 +02:00
Matej Bačo 7fbfb6266b GitHub oauth response model 2026-04-24 10:56:39 +02:00
Matej BačoandGitHub 29b700d1ec Merge pull request #11981 from appwrite/feat-public-list-endpoints
Feat: Project public list endpoints
2026-04-24 10:08:23 +02:00
Matej Bačo e3231393b9 Fix anayser 2026-04-23 16:06:45 +02:00
Matej Bačo 5beeca5a99 Placeholder test 2026-04-23 15:57:09 +02:00
Matej Bačo 4de3009f67 Fix analyser 2026-04-23 15:36:16 +02:00
Matej Bačo 4b3963512c Linter fix 2026-04-23 15:28:20 +02:00
Matej Bačo 8c634a95e4 Fix failing tests 2026-04-23 15:28:10 +02:00
Matej Bačo 7a3c001452 Re-add project removal tests 2026-04-23 15:22:40 +02:00
Matej Bačo a48fd13ced Add getPolicy + tests + move wrongly placed project tests 2026-04-23 15:19:49 +02:00
Matej Bačo 9c6ed9565e Remove tests of removed endpoints 2026-04-23 14:07:58 +02:00
Matej Bačo bdbc5b92df Fix after code review 2026-04-23 13:47:31 +02:00
Matej Bačo c246fb0f83 Project deletion tests 2026-04-23 13:41:11 +02:00
Matej Bačo a0a3849b16 Remove unsupported bulk endpoints 2026-04-23 13:37:32 +02:00
Matej Bačo b99139661e Migrate delete project endpoint 2026-04-23 13:37:19 +02:00
Matej Bačo 6d86b8fd0d Removal of project JWTs 2026-04-23 13:25:21 +02:00
Matej Bačo cef7a5197f List policies API 2026-04-23 13:24:39 +02:00
Matej Bačo c1dfeae323 Add queries to email tempaltes list 2026-04-23 13:06:05 +02:00
Matej Bačo 51fa0770a6 Add queries to mock numbers list 2026-04-23 12:43:45 +02:00
Matej BačoandGitHub d46403507c Merge pull request #11979 from appwrite/fix-membership-privacy
Fix: membership privacy bug on production
2026-04-23 10:47:54 +02:00
Matej Bačo 83724ce96f Console membership privacy test coverage 2026-04-23 10:37:35 +02:00
Matej Bačo 34930e6d67 Merge branch '1.9.x' into fix-membership-privacy 2026-04-23 10:18:32 +02:00
Matej BačoandGitHub 096f8041fd Merge pull request #11970 from appwrite/feat-mocks-public-api
Feat: Public mock phone APIs
2026-04-23 10:18:14 +02:00
Matej Bačo 9dad7cef9e Merge branch '1.9.x' into feat-mocks-public-api 2026-04-23 10:17:32 +02:00
Matej BačoandGitHub 9e23867f0a Merge pull request #11976 from appwrite/feat-auth-methods-api
Feat: Auth methods public API
2026-04-23 10:14:34 +02:00
Matej BačoGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
48353faa9b Apply suggestions from code review
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-23 10:13:01 +02:00
Matej Bačo c36b8fbabf Fix membershiip privacy bug on production 2026-04-23 10:07:32 +02:00
Luke B. SilverandGitHub 5a5cb1e74e Merge pull request #11977 from appwrite/perf/v20-investigation
perf: memoize request filter chain and V20 schema lookups
2026-04-23 07:38:03 +01:00
Chirag AggarwalandGitHub a8e6b1b683 Merge pull request #11963 from appwrite/chore/http-benchmark-comparison 2026-04-23 09:48:37 +05:30
Matej Bačo b0939b92c3 Fix failing account tests 2026-04-22 17:02:22 +02:00
Chirag Aggarwal 3d66078fe9 Increase benchmark iterations 2026-04-22 19:46:27 +05:30
Chirag Aggarwal 9a6a597710 Address benchmark hardening review 2026-04-22 19:38:48 +05:30
Chirag Aggarwal c15e8d0126 Harden benchmark failure guard 2026-04-22 19:30:01 +05:30
Chirag Aggarwal 7b25d778d4 Trim benchmark scenarios 2026-04-22 19:21:51 +05:30
loks0n 3283b0bec0 perf: memoize request filter chain and V20 schema lookups
A phpspy profile of a production databases worker showed the V20
backwards-compat request filter accounting for ~40% of in-request
samples on `databases.listDocuments` traffic. Two compounding causes:

1. `Request::getParams()` re-ran the entire filter chain on every
   invocation. The framework and app call `getParams()` several times
   per request (route param binding, `cacheIdentifier()`, action
   injection, logging), so V20's recursive schema walk executed N times
   with identical inputs.
2. Inside `V20::getRelatedCollectionKeys`, the `databases/$databaseId`
   document was fetched at every recursion frame (up to
   RELATION_MAX_DEPTH = 3), and sibling relationships pointing at the
   same related collection each did their own `getDocument` call.

This commit:

- Memoizes the post-filter params on `Request`. The cache is
  invalidated by `addFilter`, `resetFilters`, and `setRoute`. `Request`
  is constructed per HTTP request (app/http.php), so the memo is
  naturally request-scoped. Helps every request filter version, not
  just V20.
- Splits V20's walk into an entry point that resolves the database
  namespace once and a pure recursive helper.
- Caches the collection `attributes` array per
  `(databaseNamespace, collectionId)` on the filter instance, so shared
  related collections collapse to one `getDocument` call. Missing or
  errored lookups are cached as `null` to avoid retry storms.
2026-04-22 14:49:13 +01:00
ArnabChatterjee20kandGitHub 49f8d6e89c Merge pull request #11971 from appwrite/realtime-logs
Structured Logging
2026-04-22 19:10:36 +05:30
Matej Bačo a0274a7b6f Fix failing tests 2026-04-22 15:12:58 +02:00
Chirag AggarwalandGitHub 277a09847e Merge pull request #11975 from appwrite/t3code/database-tables-retry-fix
[codex] Stabilize database e2e CI retries
2026-04-22 18:34:29 +05:30
Chirag AggarwalandGitHub cdc5654c26 Merge branch '1.9.x' into chore/http-benchmark-comparison 2026-04-22 18:24:50 +05:30
Matej Bačo bb4fdefee7 New tests for auth methods (base + integration) 2026-04-22 14:51:10 +02:00
ArnabChatterjee20k c2e5bbe0f7 updated 2026-04-22 18:11:32 +05:30
Chirag Aggarwal b2d24080b9 Stabilize database e2e CI retries 2026-04-22 18:08:45 +05:30
ArnabChatterjee20k b006858d0c dedupe 2026-04-22 17:52:45 +05:30
Chirag AggarwalandGitHub a0f30f608d Merge pull request #11974 from appwrite/fix/no-rule-without-deployment 2026-04-22 17:52:02 +05:30
Matej Bačo a85c5e582c Add auth method APIs (public) 2026-04-22 14:19:04 +02:00
ArnabChatterjee20k 6d1def7716 removed redundant span attributes 2026-04-22 17:45:58 +05:30
ArnabChatterjee20k f0f1e1c412 updated 2026-04-22 17:44:18 +05:30
Chirag AggarwalandGitHub 11f85e2477 Merge branch '1.9.x' into chore/http-benchmark-comparison 2026-04-22 17:24:19 +05:30
Chirag AggarwalandGitHub dbb1d1139e Merge branch '1.9.x' into fix/no-rule-without-deployment 2026-04-22 17:20:31 +05:30
Chirag AggarwalandGitHub f0e2a8b2c4 Merge pull request #11972 from appwrite/t3code/sentry-bug-fix 2026-04-22 17:18:15 +05:30
ArnabChatterjee20k 46e778ea90 updated 2026-04-22 17:13:35 +05:30
ArnabChatterjee20k fd9fe5d9ce corrected the position 2026-04-22 17:07:53 +05:30
ArnabChatterjee20k 59e0383264 updated 2026-04-22 17:01:08 +05:30
ArnabChatterjee20k 0f81bc2da9 Refactor telemetry logging in realtime events for consistency and clarity
- Updated span logging keys to use camelCase for uniformity across connection and message events.
- Added checks to ensure project and user IDs are only logged if they are not empty, enhancing data integrity.
- Improved error handling and logging structure to maintain consistency in telemetry data.
2026-04-22 16:57:51 +05:30
ArnabChatterjee20k 17e3d03b40 Add telemetry logging for subscribed channels and queries in realtime events
- Introduced new arrays to capture subscribed channels and passed queries during connection and message events.
- Enhanced span logging to include details about channels and queries for better monitoring and analysis.
- Updated telemetry data structure to reflect the new metrics, improving traceability of realtime interactions.
2026-04-22 16:55:52 +05:30
Chirag Aggarwal c97435d95c Stabilize benchmark wait metric tags 2026-04-22 16:53:35 +05:30
ArnabChatterjee20k b2ad7237ab Add detailed telemetry logging for realtime connection events
- Introduced span logging for connection open and close events, capturing metrics such as inbound and outbound bytes, subscription counts, and response codes.
- Enhanced error handling with logging of exceptions during connection lifecycle.
- Updated the structure of the telemetry data to include project and user IDs for better traceability.
2026-04-22 16:52:54 +05:30
ArnabChatterjee20k 57d777f80a revert format 2026-04-22 16:46:18 +05:30
Chirag Aggarwal d1962dbc62 Shorten local benchmark command 2026-04-22 16:45:16 +05:30
Matej Bačo 6648a1987b Fix tests 2026-04-22 13:14:43 +02:00
ArnabChatterjee20k c60e85ca84 Merge remote-tracking branch 'origin/1.9.x' into realtime-logs 2026-04-22 16:44:34 +05:30
ArnabChatterjee20k ca1cf1982f added wide events inside the structured one logging per message instead of a discrete logs 2026-04-22 16:44:20 +05:30
Chirag Aggarwal f50ca0281b Drop dead ternary guards now that outer check ensures deployment 2026-04-22 16:43:28 +05:30
Chirag AggarwalandGitHub 2f12717f30 Merge pull request #11969 from appwrite/t3code/spec-overlap-verification 2026-04-22 16:28:34 +05:30
Chirag Aggarwal f934259c31 Skip preview rule when no deployment exists on function create 2026-04-22 16:26:41 +05:30
Chirag AggarwalandGitHub 838dbc52e3 Merge branch '1.9.x' into t3code/sentry-bug-fix 2026-04-22 16:23:25 +05:30
Chirag Aggarwal e7d9ef74c4 Fix deployment single chunk content range
Fixes CLOUD-3JN6
2026-04-22 16:05:12 +05:30
Chirag Aggarwal d106e1d5bb Fix session alert test payload 2026-04-22 15:57:32 +05:30
Matej Bačo d1ade3872e Fix failing tests 2026-04-22 12:22:00 +02:00
Chirag Aggarwal 0f64f54221 Harden benchmark rerun metrics 2026-04-22 15:49:27 +05:30
Chirag Aggarwal 00512df4ca Clean up spec enum validation reporting 2026-04-22 15:45:16 +05:30
Matej Bačo 9065d9ada4 Add mocks scopes 2026-04-22 12:13:10 +02:00
ArnabChatterjee20k e0fec8f550 updated 2026-04-22 15:42:17 +05:30
Chirag Aggarwal 205c283935 Remove unused JWT benchmark setup 2026-04-22 15:33:52 +05:30
Chirag Aggarwal 038be90969 Preserve nested items enum validation 2026-04-22 15:33:18 +05:30
Matej Bačo 355d4323fc Fix tests not running 2026-04-22 12:01:42 +02:00
Matej Bačo 7578b5644c AI review fixes 2026-04-22 12:00:15 +02:00
Chirag Aggarwal 240cdf43e5 Simplify local benchmark command 2026-04-22 15:24:47 +05:30
Chirag Aggarwal 481eaf7530 Merge remote-tracking branch 'origin/1.9.x' into t3code/spec-overlap-verification 2026-04-22 15:24:34 +05:30
Chirag Aggarwal 2390d40731 Remove specs task unit test 2026-04-22 15:24:13 +05:30
Chirag Aggarwal f75a7269c9 Address benchmark review simplifications 2026-04-22 15:23:35 +05:30
Chirag Aggarwal 4f74394e8f Fix spec enum name validation 2026-04-22 15:21:41 +05:30
Matej Bačo f770277ea5 New mock phones tests 2026-04-22 11:42:39 +02:00
Matej Bačo eeadba3b59 Add missing endpoint in email templates 2026-04-22 11:36:54 +02:00
Matej Bačo 2e42633e12 Add public mocks API for phones 2026-04-22 11:30:39 +02:00
Chirag Aggarwal affd5876ab Add spec enum service overlap validation 2026-04-22 14:49:35 +05:30
Chirag Aggarwal dfd39d3946 Tolerate benchmark cleanup failures 2026-04-22 14:25:59 +05:30
Chirag Aggarwal 7d7fcea8c0 Ensure benchmark failures fail CI 2026-04-22 14:16:12 +05:30
Chirag Aggarwal 33d3f82a58 Merge remote-tracking branch 'origin/1.9.x' into chore/http-benchmark-comparison
# Conflicts:
#	.github/workflows/ci.yml
2026-04-22 14:05:45 +05:30
Matej BačoandGitHub af531ee4f9 Merge pull request #11964 from appwrite/feat-public-project-policies
Feat: public project policies
2026-04-22 10:23:10 +02:00
Chirag Aggarwal 73a77b8dcc Show benchmark throughput 2026-04-22 13:45:50 +05:30
Chirag Aggarwal 4b1b2972e9 Merge remote-tracking branch 'origin/1.9.x' into chore/http-benchmark-comparison
# Conflicts:
#	.github/workflows/ci.yml
2026-04-22 13:34:45 +05:30
Matej Bačo 72bb6378c2 Leftover 2026-04-22 10:00:19 +02:00
Matej Bačo bfa1960d8a Remove unneeded const 2026-04-22 10:00:10 +02:00
Matej Bačo e530bf41f7 Post-merge fix 2026-04-22 09:59:00 +02:00
Matej Bačo 0d27c59cb8 Merge branch '1.9.x' into feat-public-project-policies 2026-04-22 09:57:48 +02:00
Matej BačoandGitHub 97e611029e Merge pull request #11900 from appwrite/feat-project-smtp-endpoints
Feat: Public API for project SMTP endpoints
2026-04-22 09:52:09 +02:00
Matej Bačo efc37c68ec Merge branch '1.9.x' into feat-project-smtp-endpoints 2026-04-22 09:50:08 +02:00
Chirag Aggarwal b3f305f9a8 Record storage upload wait metric 2026-04-22 13:02:16 +05:30
Chirag Aggarwal 32508e7251 Avoid reserved TablesDB benchmark column name 2026-04-22 12:53:13 +05:30
Chirag Aggarwal a98b9f2319 Handle malformed optional benchmark summaries 2026-04-22 12:06:32 +05:30
Chirag Aggarwal a0ef5968fb Document local HTTP benchmark command 2026-04-22 11:58:30 +05:30
Chirag Aggarwal bc637ad25f Merge remote-tracking branch 'origin/1.9.x' into chore/http-benchmark-comparison
# Conflicts:
#	.github/workflows/ci.yml
2026-04-22 11:38:04 +05:30
Chirag AggarwalandGitHub 5b6dd5f75a Merge pull request #11949 from appwrite/chore/phpstan-level-4 2026-04-22 11:17:21 +05:30
Jake BarnbyandGitHub 2c973387c5 Merge pull request #11967 from appwrite/fix/listrows-total-int-cast
fix: cast cached total to int in listDocuments/listRows
2026-04-22 17:02:43 +12:00
Chirag Aggarwal 3b9c604eb8 Harden benchmark comparison run 2026-04-22 09:45:51 +05:30
Jake BarnbyandClaude Opus 4.7 8a841a6971 fix: cast cached total to int in listDocuments/listRows
Redis stringifies scalars on save, so on a cache hit the `total` field
was served as a string. Flutter SDK (and any strictly-typed client) then
failed with `TypeError: "37": type 'String' is not a subtype of type 'int'`.
The cache-miss path returned an int from `count()`, which is why only
repeat requests with `ttl > 0` tripped the bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:14:30 +12:00
Chirag Aggarwal c973ca0a5d Address PHPStan level 4 review feedback 2026-04-22 09:34:03 +05:30
Chirag Aggarwal 7c486ddcef Keep benchmark comment on missing summary 2026-04-22 09:26:36 +05:30
Chirag Aggarwal cb7f2ec693 Show top benchmark request waits 2026-04-22 09:19:26 +05:30
Chirag Aggarwal 3cc7b833db Fix k6 benchmark diagnostics 2026-04-22 09:06:54 +05:30
Chirag Aggarwal 9ca84a56c9 Switch HTTP benchmark back to k6 2026-04-22 08:51:51 +05:30
Damodar LohaniandGitHub af05ad5784 Merge pull request #11953 from appwrite/CLO-4204-slow-query-hook
feat: add afterQuery hook to listRows (tablesdb) action
2026-04-22 07:25:24 +05:45
Matej Bačo 39af9c544d Remove unnessessary backwards compatibility 2026-04-21 18:22:44 +02:00
Luke B. SilverandGitHub 10b6955e98 Merge pull request #11965 from appwrite/fix/support-worker-num
fix: honor _APP_WORKERS_NUM in realtime
2026-04-21 17:14:08 +01:00
loks0nandClaude Opus 4.7 7e963e8439 fix: honor _APP_WORKERS_NUM in realtime
Realtime was ignoring _APP_WORKERS_NUM and always computing workers as
CPU × _APP_WORKER_PER_CORE, making it impossible to cap the worker count
without also changing the per-core multiplier. Prefer _APP_WORKERS_NUM
when set, falling back to the CPU × per-core calculation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:55:56 +01:00
Matej Bačo d9d5a81337 Fix 0->null backwrds compatibility 2026-04-21 17:51:31 +02:00
Matej Bačo 193891a7c9 Fix analyzer 2026-04-21 17:38:32 +02:00
Matej Bačo 70cb5ca68a linter fix 2026-04-21 17:34:59 +02:00
Matej Bačo c8a1746119 Fix existing tests 2026-04-21 17:34:44 +02:00
Matej Bačo 06eb550e98 Finalize tests 2026-04-21 16:56:00 +02:00
Matej Bačo 6c89a05a60 Fix 0 session to mean unlimited 2026-04-21 16:38:53 +02:00
Matej BačoandGitHub 1b993ede0f Merge branch '1.9.x' into feat-project-smtp-endpoints 2026-04-21 16:15:00 +02:00
Matej Bačo 648ffcdcfa Fix event labels 2026-04-21 16:07:52 +02:00
Matej Bačo 0be94a7aff Fix tests 2026-04-21 15:57:30 +02:00
Matej Bačo eba145ee2d More integration tests 2026-04-21 15:13:27 +02:00
Matej Bačo 99d230c70d Integration tests 2026-04-21 14:39:02 +02:00
Chirag Aggarwal 4ac1b68bbc Fix OpenAPI enum keys analysis 2026-04-21 18:03:18 +05:30
Chirag Aggarwal 211ac32080 Guard benchmark account-dependent flows 2026-04-21 17:59:09 +05:30
Chirag Aggarwal 30bf9deae0 Remove benchmark failure rows from output 2026-04-21 17:54:36 +05:30
Chirag Aggarwal 196b04a39c Polish benchmark comment details 2026-04-21 17:49:50 +05:30
Matej Bačo 6adabae620 Add policy tests 2026-04-21 14:05:46 +02:00
Matej Bačo 5f9dc0fcd8 Req & res filters, review fixes 2026-04-21 13:58:36 +02:00
Chirag Aggarwal b9d01617a4 Address benchmark review hardening 2026-04-21 17:24:26 +05:30
Chirag Aggarwal 9c65609d73 Check benchmark summary writes 2026-04-21 17:13:00 +05:30
Chirag AggarwalandGitHub da4dcd8505 Merge branch '1.9.x' into chore/phpstan-level-4 2026-04-21 17:08:46 +05:30
Matej Bačo d0f853d4cd Add more project policies 2026-04-21 13:38:27 +02:00
Chirag Aggarwal 63b2a1fb7f Harden benchmark baseline reporting 2026-04-21 17:07:05 +05:30
Chirag Aggarwal 30cfbb2d99 Polish benchmark reporting 2026-04-21 16:58:31 +05:30
Matej Bačo 2f272f0480 Cleanup unused markdowns descriptions 2026-04-21 13:27:08 +02:00
Matej Bačo 4317ee5617 Move some of auth settings to project policies 2026-04-21 13:11:42 +02:00
Chirag Aggarwal ef08d5a04c Run before benchmark with base image 2026-04-21 16:33:09 +05:30
Chirag Aggarwal 566eebfaec Fix benchmark PNG fixture 2026-04-21 16:31:57 +05:30
Chirag Aggarwal 51bc3dc1d5 Migrate HTTP benchmark to PHP 2026-04-21 16:22:04 +05:30
Chirag Aggarwal 83f182b444 Address benchmark review feedback 2026-04-21 16:10:08 +05:30
Chirag Aggarwal dcd01a8fb0 Tidy benchmark PR comment 2026-04-21 16:06:30 +05:30
Chirag Aggarwal 6aeb2d2be0 Fix benchmark before branch comparison 2026-04-21 15:51:30 +05:30
Chirag Aggarwal 2cfe40e98e Compare benchmark against base branch 2026-04-21 15:11:24 +05:30
Chirag Aggarwal 15e45df81e Address HTTP benchmark review feedback 2026-04-21 15:07:20 +05:30
Chirag Aggarwal e4f74a3fb1 Run curated HTTP benchmark in CI 2026-04-21 14:56:33 +05:30
Chirag Aggarwal 774a0d7022 Improve HTTP benchmark coverage 2026-04-21 14:48:12 +05:30
Damodar LohaniandGitHub 1dc9217c65 Merge branch '1.9.x' into CLO-4204-slow-query-hook 2026-04-21 14:31:45 +05:45
Damodar Lohani 50bd2877f4 chore: shorten afterQuery docblock 2026-04-21 08:46:21 +00:00
Damodar Lohani f465e2267a chore: tighten afterQuery docblock 2026-04-21 08:43:20 +00:00
Damodar Lohani b4f1652286 refactor: simplify afterQuery DB timing to single wall-clock bracket
Replaces the per-call $measure closure with a single $dbStart
timestamp taken right before the fetch block and a single subtraction
right after it. Drops 6 lines of HOF indirection plus the $measure
variable, at the cost of including cache GET/SET time (~0.5–5ms) in
measurements when ttl > 0. For slow-query logging at a 100ms+
threshold that noise is negligible, and the default ttl=0 path has
no cache ops at all so the measurement is pure DB engine time.

The bracket captures the cursor lookup, find/count, and transaction
state calls — everything between "query parsed" and "fetch done",
as intended. processDocument's post-fetch relationship work is still
outside the bracket, matching the original design.
2026-04-21 08:41:11 +00:00
Damodar Lohani 4e4860e7f8 refactor: move afterQuery hook into base listDocuments action
Moves the DB-duration measurement and afterQuery() hook from the
tablesDB-specific Rows/XList into the shared
Databases/Collections/Documents/XList base. Because TablesDB Rows and
DocumentsDB Documents both extend the legacy listDocuments base, a
single override now covers all three endpoints: legacy
listDocuments, listDocumentsDBDocuments, and tablesDB listRows.

TablesDB Rows drops the ~200-line action() duplicate and keeps only
the path/params/SDK overrides it needs, plus the extra
->inject('utopia') so its injection chain matches the new base action
signature. DocumentsDB Documents gets the same one-line inject
addition. Net -165 lines of duplication removed.

Behaviour is unchanged for CE (afterQuery() is a no-op); downstream
distributions overriding afterQuery() now observe every list-documents
/ list-rows call site for free.
2026-04-21 08:28:12 +00:00
Matej Bačo cce04b3bd1 Improve test coverage 2026-04-21 10:28:11 +02:00
Harsh MahajanandGitHub 7568964b7c Merge pull request #11905 from appwrite/feat-add-telemetry-for-ss-success-rates
feat(sites): add telemetry to ss rates
2026-04-21 13:54:05 +05:30
Damodar LohaniandGitHub 82af29c0cf Merge branch '1.9.x' into CLO-4204-slow-query-hook 2026-04-21 14:04:29 +05:45
Harsh MahajanandGitHub c6672e93cf Merge branch '1.9.x' into feat-add-telemetry-for-ss-success-rates 2026-04-21 13:07:40 +05:30
ArnabChatterjee20kandGitHub ac905a80b5 Merge pull request #11954 from appwrite/realtime-time-metric
Realtime time metric
2026-04-21 11:14:27 +05:30
ArnabChatterjee20k 5df65d5417 updated 2026-04-21 10:58:34 +05:30
ArnabChatterjee20k abd6f0add2 updated 2026-04-21 10:52:21 +05:30
ArnabChatterjee20k 25854c84f5 Merge remote-tracking branch 'origin/1.9.x' into realtime-time-metric 2026-04-21 10:44:30 +05:30
Damodar Lohani 5724d19426 Merge remote-tracking branch 'origin/CLO-4204-slow-query-hook' into CLO-4204-slow-query-hook 2026-04-21 04:34:38 +00:00
Damodar Lohani 7c84337c5f Merge remote-tracking branch 'origin/1.9.x' into CLO-4204-slow-query-hook 2026-04-21 04:32:28 +00:00
Matej Bačo b1d37bc4be Fix remaining test failures 2026-04-20 23:03:16 +02:00
Matej Bačo 4808cad081 Fix most of Project tests 2026-04-20 23:01:53 +02:00
Matej Bačo 72cd7671c4 Fix projects tests 2026-04-20 23:01:00 +02:00
Matej Bačo c3e411fcaa Fix tests 2026-04-20 22:47:47 +02:00
Matej Bačo 8c31e9f206 Server error fixes 2026-04-20 22:36:47 +02:00
Matej Bačo a4ad1b6df3 Code quality improvs 2026-04-20 22:35:35 +02:00
Matej Bačo 9e94f15f02 Finalize tests 2026-04-20 22:23:34 +02:00
Matej Bačo e27719108b Add tests 2026-04-20 22:15:03 +02:00
Matej Bačo 8bf2f54a51 Leftovers 2026-04-20 22:04:46 +02:00
Matej Bačo 8c03db70e9 Finalize templates design 2026-04-20 22:04:20 +02:00
Matej Bačo 8ea69e0321 Fix password visibility test 2026-04-20 16:57:23 +02:00
Matej Bačo dfec2b3cb7 Improve test coverage 2026-04-20 16:11:22 +02:00
ArnabChatterjee20kandGitHub 69d778ab05 Merge pull request #11946 from appwrite/migration-via-api
added project region
2026-04-20 19:40:23 +05:30
Matej Bačo 848f09956e Improve backwards compatibility test coverage 2026-04-20 15:36:49 +02:00
Matej Bačo 51f50b161c Improved backwards compatibility 2026-04-20 15:28:05 +02:00
ArnabChatterjee20kandGitHub 53d3713bfe Merge pull request #11957 from appwrite/realtime-add-new-messages
Realtime add new messages
2026-04-20 18:46:25 +05:30
ArnabChatterjee20k b8385fe927 updated 2026-04-20 18:27:48 +05:30
Matej Bačo 52e3319a86 Linter fix 2026-04-20 14:50:12 +02:00
Matej Bačo 8b41aed919 Post-merge removal 2026-04-20 14:49:43 +02:00
ArnabChatterjee20k b2233193d5 updated 2026-04-20 18:19:12 +05:30
Matej Bačo ba4430801d Merge branch 'feat-project-templates-api' into feat-project-smtp-endpoints 2026-04-20 14:49:04 +02:00
Matej Bačo 1c1ec43150 Removeal post-merge 2026-04-20 14:47:01 +02:00
Matej Bačo 2f62cced0a Merge branch '1.9.x' into feat-project-smtp-endpoints 2026-04-20 14:46:42 +02:00
Matej Bačo afb8f70316 Fix tests 2026-04-20 14:42:52 +02:00
ArnabChatterjee20k 78eeac6d14 Add unsubscribe functionality and enhance subscription handling in Realtime tests 2026-04-20 17:38:01 +05:30
ArnabChatterjee20k 9f65177649 Add unsubscribe functionality to Realtime adapter 2026-04-20 17:37:54 +05:30
ArnabChatterjee20k e9ea39a822 Enhance Realtime adapter: support union of channels/roles on subscription and add unsubscribeSubscription method 2026-04-20 17:37:45 +05:30
Matej Bačo 2097b0a0b0 Better support for post-smtp changes 2026-04-20 14:04:53 +02:00
Matej Bačo 78ef52cc9e Manual QA fixes 2026-04-20 13:11:11 +02:00
Matej Bačo f040a4dc31 More backwards compatibility 2026-04-20 11:58:55 +02:00
Matej Bačo 56385ce167 Backwards compatibility 2026-04-20 11:48:45 +02:00
Matej Bačo bc592903db Support reply to name 2026-04-20 11:47:06 +02:00
Matej Bačo 06a2d48deb Linter fix 2026-04-20 11:29:13 +02:00
Matej Bačo c4d9c3dc4f Improve endpoint quality 2026-04-20 11:28:21 +02:00
Damodar LohaniandGitHub b36f1edeb0 Merge branch '1.9.x' into CLO-4204-slow-query-hook 2026-04-20 14:04:10 +05:45
Damodar Lohani eef443f07e chore: bump utopia-php/database to 5.3.22 for Query::fingerprint 2026-04-20 07:53:36 +00:00
ArnabChatterjee20k 62f7f25cb5 updated 2026-04-20 12:18:38 +05:30
ArnabChatterjee20k 4b94d14f1e updated time metric 2026-04-20 12:13:53 +05:30
ArnabChatterjee20k 12f76d74b1 added subscriptions telemetry and worker labelling to connections and subscriptions 2026-04-20 11:52:43 +05:30
Chirag AggarwalandGitHub 9f504cd065 Merge pull request #11950 from appwrite/add-claude-plugin 2026-04-20 11:48:40 +05:30
Jake BarnbyandGitHub 9dc27e9ca5 Merge pull request #11894 from appwrite/chore-remove-shared-v1
refactor: Remove shared tables V1/V2 versioning
2026-04-20 18:16:12 +12:00
ArnabChatterjee20k 2bff4192ee added telemetry for the connected clients 2026-04-20 11:45:05 +05:30
ArnabChatterjee20k a9fb17b067 added telemetry for message arriaval delay 2026-04-20 11:39:50 +05:30
Jake Barnby db3d00b1da Merge remote-tracking branch 'origin/1.9.x' into chore-remove-shared-v1 2026-04-20 18:04:26 +12:00
Atharva Deosthale 5ab42b8d32 update composer lock 2026-04-20 11:26:31 +05:30
ArnabChatterjee20k 8de7b41929 updated 2026-04-20 11:06:07 +05:30
Damodar Lohani a5c0a920ba feat: add afterQuery hook to list-documents/rows action
Wrap each database call (find, count, transaction list/count) with a
measuring closure so the actual DB duration is known — cache hits
report near-zero, cache misses report only the DB time, not cache
save / response serialization.

After the response is sent, invoke a protected afterQuery() hook with
the measured duration, the database/collection documents, and both
parsed + raw query arrays. CE impl is a no-op; downstreams (e.g.,
cloud) can override it to log slow queries without relying on HTTP
shutdown hooks or route-path matching.

Exceptions from afterQuery are swallowed so observability never
breaks the response.
2026-04-20 05:32:49 +00:00
Chirag Aggarwal 37a2b1cbd9 fix: restore executions limit cleanup behind a runtime env flag
Per review feedback on the PHPStan cleanup, the two `if
($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE)`
blocks in `app/controllers/general.php` and
`src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php`
were load-bearing feature flags, not dead code. Removing them silently
dropped the ability to turn the cleanup on later.

Changes:

- Convert `ENABLE_EXECUTIONS_LIMIT_ON_ROUTE` from
  `const ... = false;` to a `define()` backed by the new
  `_APP_EXECUTIONS_LIMIT_ON_ROUTE` env var (defaults to `disabled`).
  PHPStan can no longer fold the `&&` away since the value is now
  runtime-resolved, so the guarded blocks are live again.
- Restore the `/* cleanup */` block in the `router()` helper in
  `app/controllers/general.php`.
- Restore the two cleanup blocks in `Functions/Http/Executions/Create.php`
  (one on the async-scheduled return path, one on the sync-response
  path), and re-add the `DeleteEvent $queueForDeletes` /
  `int $executionsRetentionCount` injections plus the
  `Appwrite\Event\Delete` import.

Runtime behavior is identical to main (flag off by default); operators
can now flip it via env without a code change.
2026-04-20 08:54:31 +05:30
Matej Bačo 6b66923f18 Fix delete response placeholder audit label 2026-04-19 19:36:24 +02:00
Chirag Aggarwal adb4e4ef36 ci: fix benchmark by pulling compose from GitHub raw for the latest tag
`https://appwrite.io/install/compose` now returns a 308 redirect to the
HTML install docs (`/docs/advanced/self-hosting/installation`) instead
of serving the compose file, so the Benchmark job's "Installing latest
version" step was downloading 0 bytes and `docker compose up -d` died
with "empty compose file". This has been failing the Benchmark job on
every recent PR, not just this one.

Resolve the latest release tag via the GitHub API, then fetch the
compose file and `.env` from `raw.githubusercontent.com` at that tag.
Switched both curl calls to `-fsSL` so they fail loudly on non-2xx
responses or redirect loss instead of silently writing empty files.
2026-04-19 20:34:51 +05:30
Atharva Deosthale 56165ee3d9 add claude plugin to static sdk 2026-04-19 18:39:19 +05:30
Chirag Aggarwal d86258a6f6 fix: restore runtime guards and widen types missed by PHPStan cleanup
Three follow-ups from CI that the level-4 pass got wrong:

1. `account.php` / `users.php`: `Document::find()` returns `mixed`
   (specifically `Document|false` in practice), not `Document`. The
   earlier `@var Document $oldTarget` docblocks were lies, and the
   runtime `instanceof Document` guards were load-bearing — removing
   them caused `Call to a member function isEmpty() on false` 500s
   on the `PATCH /v1/users/:id/email` and `/phone` endpoints (and the
   analogous `/v1/account/email`, `/v1/account/phone` flows). Dropped
   the misleading `@var` docblocks and restored
   `$oldTarget instanceof Document && !$oldTarget->isEmpty()`.

2. `Installer/Runtime/Config::setEnabledDatabases()` is a boundary
   that actually takes arbitrary user/compose input — not a trusted
   `string[]`. The `is_string($v)` filter was covering for that, and
   `ConfigTest::testSetEnabledDatabasesFiltersInvalid` explicitly
   asserts it. Widened the PHPDoc to `array<mixed>` and restored
   `is_string($v) && $v !== ''` in the filter.

3. `OAuth2/Apple::getAppSecret()` wrapped `json_decode` in a
   `try/catch (\Throwable)` — but `json_decode` without
   `JSON_THROW_ON_ERROR` returns `null` on failure, it doesn't throw.
   PHP 8.3's PHPStan flagged the catch as dead (PHP 8.5 didn't, which
   is why it slipped through locally). Replaced with
   `if (!\is_array($secret)) throw`, which preserves the original
   "invalid secret" guard.
2026-04-19 17:52:51 +05:30
Chirag Aggarwal d2230f8fe7 chore: bump PHPStan to level 4 and fix all new errors
Raises `phpstan.neon` level from 3 to 4 and fixes the 549 new errors
that level 4 surfaces across 157 files. Fixes are root-cause — no
`@phpstan-ignore`, no `@var` casts, no baseline entries, no widened
types. A handful of latent bugs were fixed along the way:

- `app/controllers/general.php`: path-traversal guard was negating
  `\substr(...)` before the strict comparison (`!\substr(...) === $base`
  was always `false === $base`). Rewritten as `\substr(...) !== $base`.
- `src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php`
  and `.../TablesDB/Logs/XList.php`: were importing the raw Matomo
  `DeviceDetector` (whose `getDevice()` returns `?int`) but treating the
  result as an array with `deviceName/deviceBrand/deviceModel` keys.
  Swapped to `Appwrite\Detector\Detector`, matching the wrapper already
  used a few lines below for `$os`/`$client`.
- `src/Appwrite/Platform/Modules/Functions/Workers/Builds.php`: a match
  key was checking `$resourceKey === 'functions'` when `$resourceKey`
  is `'functionId'|'siteId'` — always false. Switched to the intended
  `$resource->getCollection() === 'functions'` check.
- `src/Appwrite/OpenSSL/OpenSSL.php`: `encrypt()` return type tightened
  to `string|false` to match `openssl_encrypt`; this lets callers'
  `=== false` error handling remain meaningful.
- `app/controllers/api/messaging.php`: removed a dead
  `array_key_exists('from', [])` branch in the Msg91 provider (empty
  array literal; branch was unreachable).

Large cleanup categories across the 549 fixes:
- Removed redundant `?? default` on array offsets and expressions that
  PHPStan now knows are non-nullable.
- Removed unreachable statements (mostly `return;` after `throw` or
  `markTestSkipped()`).
- Removed redundant `is_array`/`is_string`/`is_bool`/`instanceof` checks
  on already-narrowed types.
- Added `default =>` arms (or throwing arms) to non-exhaustive matches
  on `string`/`mixed` input.
- Removed dead `$document === false` branches where method return types
  were tightened to non-nullable `Document`.
- Removed unused properties (`$version` on Etsy/Zoom OAuth2, `$paths` on
  Installer State, `$source` on MigrationsWorker, `$account2` on two
  GraphQL auth tests), unused traits (`ApiVectorsDB`, `DatabaseFixture`),
  and an unused `cleanupStaleExecutions` task method.
- Replaced `assertTrue(true)` and redundant `assertIsArray`/`assertIsString`/
  `assertNotNull` assertions with `addToAssertionCount(1)` or
  `assertNotEmpty` where the runtime type was already known.
2026-04-19 17:31:20 +05:30
Matej BačoandClaude Opus 4.7 69d53cb2d4 Remove unused email template list response model
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:03:36 +02:00
Matej Bačo 447375dcbf Fix tests 2026-04-19 10:53:11 +02:00
Matej Bačo afab349a77 self review fixes 2026-04-19 10:43:57 +02:00
Matej Bačo 2a95cfd5a3 Final template API rework 2026-04-19 10:35:57 +02:00
Matej BačoandClaude Opus 4.7 8a1f8c71b2 Temporary removal of listEmailTemplates
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:15:48 +02:00
ArnabChatterjee20k 2793bcac38 updated 2026-04-18 23:11:04 +05:30
ArnabChatterjee20k 13f48797d4 added project region 2026-04-18 23:01:41 +05:30
Matej Bačo 0a71efc244 improve test coverage 2026-04-18 11:23:53 +02:00
Matej Bačo fcc7a56f4a Fix list endpoint 2026-04-18 11:01:26 +02:00
Luke B. SilverandGitHub 0aab3e9a43 Merge pull request #11941 from appwrite/fix/avif
fix: include project ID in storage preview cache key
2026-04-17 20:28:11 +01:00
Matej Bačo be5eeb1aba Fix failing tests 2026-04-17 19:50:34 +02:00
loks0nandClaude Sonnet 4.6 08b43dce50 fix: ksort after project injection to keep cache key order stable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 18:45:00 +01:00
loks0nandClaude Sonnet 4.6 ad3bdee6c1 fix: include project ID in storage preview cache key
Cache key never included the project ID, so two projects with the same
bucketId, fileId, and transform params would share a cache key. On a
cache hit, Appwrite re-validates the bucket from the cached resourceType
(another project's bucket), which doesn't exist in the requesting
project's DB, throwing storage_bucket_not_found.

Fix: add 'project' to cache.params on the preview route (covers query
param case) and fall back to the X-Appwrite-Project header in
cacheIdentifier() for authenticated requests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 18:34:13 +01:00
Matej Bačo e00efaea68 Fix bug 2026-04-17 18:15:49 +02:00
Luke B. SilverandGitHub be214f7968 Merge pull request #11940 from appwrite/fix/avif
fix: do not cache error responses for storage preview, bump utopia-php/image to 0.8.5
2026-04-17 17:03:29 +01:00
Matej Bačo 64d182ac6a Add tests for templates 2026-04-17 17:49:20 +02:00
Matej Bačo bb38bf4248 Improve code quality 2026-04-17 17:42:46 +02:00
loks0nandClaude Sonnet 4.6 956285d522 fix: do not cache error responses for storage preview, bump utopia-php/image to 0.8.5
Cache write hook now checks HTTP status code before writing to prevent
failed AVIF (or any other) conversions from poisoning the cache.
Bumps utopia-php/image to 0.8.5 which fixes AVIF/HEIC output by using
native Imagick instead of the deprecated magick convert shell command.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 16:37:25 +01:00
Matej Bačo dc704fdb51 Improved xlist endpoint 2026-04-17 17:27:19 +02:00
Matej Bačo e388d2f6a3 List tempaltes endpoint 2026-04-17 17:22:55 +02:00
Aditya OberaiandGitHub fc62ef2fcc Merge pull request #11926 from appwrite/update-react-admin-template
Update React Admin template metadata
2026-04-17 20:32:45 +05:30
Matej Bačo b01ec03723 Fix analyze bug 2026-04-17 17:01:27 +02:00
Matej Bačo 489b2c4e21 Add new scopes 2026-04-17 16:45:04 +02:00
Matej Bačo 1a46fc2006 Move template APIs under project API 2026-04-17 16:43:17 +02:00
Luke B. SilverandGitHub c1b7aff2d9 Merge pull request #11934 from appwrite/feat/build-timeout
feat: use buildTimeout from message payload in build worker
2026-04-17 15:07:27 +01:00
loks0nandClaude Sonnet 4.6 7df1814203 refactor: rename buildTimeout to timeout in payload and buildDeployment param
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 14:26:38 +01:00
loks0nandClaude Sonnet 4.6 8f39783d7a refactor: remove jwtExpiry alias, use timeout directly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 14:26:38 +01:00
loks0nandClaude Sonnet 4.6 4043153df3 fix: pass buildTimeout as parameter to buildDeployment to fix PHPStan error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 14:26:38 +01:00
loks0nandClaude Sonnet 4.6 9765c7f0e3 feat: use buildTimeout from message payload in build worker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 14:26:38 +01:00
Matej BačoandGitHub eddd159af8 Merge pull request #11932 from appwrite/feature/remove-/status-from-project-paths-upgrade-to-platform-0
Remove /status from project endpoint paths; upgrade to platform 0.13
2026-04-17 15:14:58 +02:00
ArnabChatterjee20kandGitHub 17de886296 Merge pull request #11936 from appwrite/realtime-time-metric
Realtime time metric
2026-04-17 18:31:51 +05:30
Matej Bačo 27b0e48296 Remove Status suffix from project event names
- project.updateServiceStatus → project.updateService
- project.updateProtocolStatus → project.updateProtocol
2026-04-17 14:53:59 +02:00
ArnabChatterjee20k 6b2054d0b5 Merge remote-tracking branch 'origin/realtime-time-metric' into realtime-time-metric 2026-04-17 18:02:18 +05:30
ArnabChatterjee20k df0f7ba581 added bucket boundary 2026-04-17 18:02:04 +05:30
Matej Bačo c484c487a9 Update tests 2026-04-17 13:19:20 +02:00
Matej Bačo 47f3ab930b Remove /status from project paths; Upgrade to platform 0.13 2026-04-17 13:14:34 +02:00
Matej BačoandGitHub c8c3c68b0e Merge pull request #11912 from appwrite/feat-fallback-email-template
Feat: Fallback email custom template
2026-04-17 12:49:16 +02:00
ArnabChatterjee20kandGitHub facae65f08 Merge pull request #11927 from appwrite/realtime-time-metric
Added delay metric
2026-04-17 16:16:16 +05:30
ArnabChatterjee20kandGitHub ef6711e317 Merge branch '1.9.x' into realtime-time-metric 2026-04-17 16:00:18 +05:30
Matej BačoandGitHub e06b06a21b Merge branch '1.9.x' into feat-fallback-email-template 2026-04-17 11:53:40 +02:00
Matej Bačo c97dd78335 Fix tests 2026-04-17 11:40:05 +02:00
Matej Bačo bf9bb22ac5 New tests 2026-04-17 11:30:24 +02:00
Matej Bačo 1b826df8f9 Non-URL locale to allow optional 2026-04-17 11:24:59 +02:00
Matej Bačo 11f23fdcfa Rework email templates PR after discussions 2026-04-17 10:52:21 +02:00
ArnabChatterjee20k b5ec92964c updated telemetry 2026-04-17 14:08:42 +05:30
ArnabChatterjee20k 71b74e21a3 added delay metric 2026-04-17 13:36:48 +05:30
Aditya Oberai 1e797b3f01 Update React Admin template metadata 2026-04-16 17:00:28 +00:00
Jake BarnbyandGitHub f1b2dd7335 Merge pull request #11925 from appwrite/atharva/compose-fixes-appwrite
Self hosted installer compose fixes
2026-04-17 00:21:50 +12:00
Chirag AggarwalandGitHub 50c379c5c3 Merge pull request #11924 from appwrite/feat/specs-provider-repo-list-discriminator 2026-04-16 16:57:37 +05:30
Atharva Deosthale 463e5acf50 compose fixes 2026-04-16 16:57:19 +05:30
Chirag Aggarwal 807e8bec8b feat(specs): add discriminator for provider repository list response union
Add ProviderRepositoryFrameworkList and ProviderRepositoryRuntimeList
model classes with conditions and type field so the listRepositories
endpoint's oneOf response gets a discriminator on the type property.
2026-04-16 16:29:42 +05:30
Harsh MahajanandGitHub f167049b51 Merge branch '1.9.x' into feat-add-telemetry-for-ss-success-rates 2026-04-16 15:23:45 +05:30
harsh mahajan 93b9500a95 align it with cloud pattern 2026-04-16 15:22:45 +05:30
Chirag AggarwalandGitHub 935c1e40eb Merge pull request #11921 from appwrite/feat/specs-discriminator-unions 2026-04-16 14:03:54 +05:30
Chirag Aggarwal e472d98fe3 Revert "refactor(specs): rename x-propertyNames/x-mapping to x-discriminator-properties/x-union-typemap"
This reverts commit 05d70f8826.
2026-04-16 13:55:36 +05:30
Matej Bačo 7fe65eec57 Restruture endpoints 2026-04-16 10:23:31 +02:00
Matej Bačo 4cf375de6d Re-add removed test 2026-04-16 10:17:08 +02:00
Matej Bačo 19d0eb66c0 Fix tests 2026-04-16 10:09:38 +02:00
Chirag Aggarwal 05d70f8826 refactor(specs): rename x-propertyNames/x-mapping to x-discriminator-properties/x-union-typemap 2026-04-16 13:32:05 +05:30
Chirag Aggarwal 98ec9e45c4 fix(specs): narrow Detection type enum to each subclass's own value
Each Detection subclass now declares only its own type value in the enum
rather than sharing the full ['runtime', 'framework'] list. This prevents
SDK validators from accepting invalid values on concrete models.
2026-04-16 13:16:13 +05:30
Chirag Aggarwal 6dc17c91bc trigger greptile 2026-04-16 13:08:14 +05:30
Chirag Aggarwal 1493b7b8a6 feat(specs): unified discriminator with compound support and algo conditions
Unify getDiscriminator to produce a single discriminator object for both
single-key and compound cases. Single-key returns standard {propertyName,
mapping}. Compound falls back to extending the object with x-propertyNames
and x-mapping for multi-property discrimination.

Simplify call sites: OpenAPI3 uses 'discriminator', Swagger2 uses
'x-discriminator' — no more split keys.

Add conditions to all 7 Algo models (AlgoArgon2, AlgoBcrypt, AlgoMd5,
AlgoPhpass, AlgoScrypt, AlgoScryptModified, AlgoSha) to enable
discriminator generation for hashOptions unions.
2026-04-16 13:02:57 +05:30
Chirag Aggarwal 965836c8b4 fix(specs): use swagger discriminator extension mapping 2026-04-16 12:28:53 +05:30
Chirag Aggarwal 4545989c91 fix(specs): remove type rule from list models, keep only on specific models 2026-04-16 12:22:37 +05:30
Chirag Aggarwal b71d42d226 fix(specs): rename getDisciminator typo and extract shared model resolution
Fix misspelled method name (getDisciminator -> getDiscriminator) across
Format, OpenAPI3, and Swagger2. Extract duplicated model-resolution
lambda into Format::resolveModels(). Fix copy-pasted descriptions in
ProviderRepository list models.
2026-04-16 11:29:16 +05:30
Chirag Aggarwal 945cdb3a99 refactor(specs): inline model resolution 2026-04-16 11:16:25 +05:30
Chirag Aggarwal a0db023860 refactor(specs): simplify discriminator resolution 2026-04-16 11:15:08 +05:30
Chirag Aggarwal 6a7280e7dd refactor(specs): inline discriminator condition checks 2026-04-16 11:12:43 +05:30
Chirag Aggarwal 680cb04de7 feat(specs): add discriminators for polymorphic responses 2026-04-16 11:07:07 +05:30
Damodar LohaniandGitHub 6e50ed050d Merge pull request #11787 from appwrite/CLO-4175-allow-delete-with-memberships
Allow deleting user account with active memberships
2026-04-16 08:16:23 +05:45
Damodar Lohani f78b5c6596 Merge remote-tracking branch 'origin/1.9.x' into CLO-4175-allow-delete-with-memberships 2026-04-16 01:16:49 +00:00
Matej Bačo 55001a7daa New integration tests 2026-04-15 19:27:26 +02:00
Matej Bačo 6d2876ab26 New E2E tests 2026-04-15 19:01:35 +02:00
Matej Bačo b510194f00 Expose "worldwide" locale 2026-04-15 18:57:37 +02:00
Matej Bačo 8fd1c5d620 Remove worldwide to not be user-facing 2026-04-15 18:54:18 +02:00
Matej Bačo 590f063694 Remove remaining sms leftover 2026-04-15 18:40:29 +02:00
Matej Bačo 90e1433878 Fix agent mistake 2026-04-15 18:38:08 +02:00
Matej Bačo 2b42487198 Linter fix 2026-04-15 18:30:06 +02:00
Matej Bačo 53ed9462bd More cleanup of sms templates 2026-04-15 18:29:43 +02:00
Matej Bačo 0da185e689 Refactor fixes 2026-04-15 18:17:55 +02:00
Matej Bačo dc39af50a1 Support for worldwide fallback custom template for all project emails 2026-04-15 18:05:46 +02:00
Matej Bačo 6da132db46 Remove SMS templates and support null locale for mail templates 2026-04-15 18:05:27 +02:00
Matej BačoandGitHub b2a02e3807 Merge pull request #11909 from appwrite/fix-protocol-exception
Fix: protocol & service exception
2026-04-15 16:59:49 +02:00
Matej Bačo e7f78b3e01 Fix shutdown event errors 2026-04-15 15:29:05 +02:00
Harsh MahajanandGitHub 682a56c03c Merge branch '1.9.x' into feat-add-telemetry-for-ss-success-rates 2026-04-15 18:46:07 +05:30
Matej Bačo 7376c5b517 Fix protocol endpoint causing InvalidArgumentException 2026-04-15 15:09:40 +02:00
harsh mahajan 730c62bda6 remove usage stats 2026-04-15 18:35:09 +05:30
ArnabChatterjee20kandGitHub db84c79802 Merge pull request #11907 from appwrite/update-events-not-firing
added backward compat
2026-04-15 18:32:21 +05:30
ArnabChatterjee20k 6ba810a4da fix unit tests 2026-04-15 17:49:33 +05:30
ArnabChatterjee20k 6d9b787816 updated string replacement 2026-04-15 17:38:21 +05:30
ArnabChatterjee20k 7b8fb409b1 added database filtering 2026-04-15 17:33:57 +05:30
ArnabChatterjee20k 1fb78115e8 added backward compat 2026-04-15 17:23:18 +05:30
harsh mahajan 9567f1f4e8 Merge branch 'origin/1.9.x' into feat-add-telemetry-for-ss-success-rates 2026-04-15 17:21:32 +05:30
Chirag AggarwalandGitHub 49c93c635d Merge pull request #11851 from appwrite/chore-migrate-audits-certificates-screenshots-to-publishers 2026-04-15 15:13:18 +05:30
Chirag AggarwalandGitHub 5010cdedbb Merge pull request #11903 from appwrite/fix/ci-mongodb-retry 2026-04-15 14:33:15 +05:30
Chirag AggarwalandGitHub 13413c05c3 Merge pull request #11880 from appwrite/fix/graphql-coroutine-safe-response 2026-04-15 14:14:57 +05:30
Chirag Aggarwal dc185be836 collapse 2026-04-15 13:52:20 +05:30
Chirag AggarwalandGitHub 29be9b6019 Merge branch '1.9.x' into chore-migrate-audits-certificates-screenshots-to-publishers 2026-04-15 13:26:52 +05:30
ArnabChatterjee20kandGitHub 1247b1c719 Merge pull request #11852 from appwrite/docsdb-fixes
Docsdb fixes
2026-04-15 13:18:41 +05:30
harsh mahajan 23233d0dcb add try catch 2026-04-15 11:59:02 +05:30
harsh mahajan 37540aa4f5 addressed comments 2026-04-15 11:52:39 +05:30
harsh mahajan db0a971384 feat(sites): add screenshot success telemetry to usage stats 2026-04-15 11:43:53 +05:30
Chirag AggarwalandGitHub 445e86361c Merge pull request #11904 from appwrite/fix/sdk-push-no-force 2026-04-15 10:27:40 +05:30
Chirag Aggarwal 80197b566c fix: replace force-push with regular push in SDK release task
The SDK push task used `git push --force-with-lease` which fails on
repos with branch protection rules that disallow force pushes. Instead,
checkout the existing remote dev branch and commit on top of it so a
regular push is always a fast-forward.
2026-04-15 10:22:50 +05:30
Chirag Aggarwal 8671533878 fix: remove orphaned docblock from deleted test 2026-04-15 10:13:37 +05:30
Chirag Aggarwal f51f02375a test: remove flaky concurrent session race condition test
testEmailPasswordSessionNotCorruptedByConcurrentRequests relies on
timing-sensitive curl_multi orchestration with hardcoded delays to
reproduce a cache race window. This makes it inherently flaky in CI
where resource pressure shifts the timing unpredictably.
2026-04-15 09:52:32 +05:30
Chirag Aggarwal d40df613de fix: run ProjectWebhooks tests sequentially in CI
ProjectWebhooks tests have shared state dependencies (e.g. index
creation must complete before assertions). Running with --functional
(parallel methods) causes flaky failures where indexes are still
'processing' instead of 'available'.
2026-04-15 09:51:26 +05:30
ArnabChatterjee20kandGitHub ebc3febc38 Merge branch '1.9.x' into docsdb-fixes 2026-04-15 09:42:05 +05:30
Chirag Aggarwal e77bfae091 fix: add restart policy to MongoDB container for flaky CI starts
MongoDB's official Docker entrypoint uses a two-phase startup: a
temporary mongod for user/db init, then the real mongod. Under CI
resource pressure the port may not be released between the two
phases, causing mongod to exit with code 48 (address already in use).

Adding restart: on-failure:3 lets Docker handle the transient failure
natively. On restart the data directory already exists so the
entrypoint skips the two-phase init entirely, avoiding the race.
2026-04-15 09:19:02 +05:30
Chirag Aggarwal f9efd803c1 refactor: remove unnecessary IIFE wrappers in resolver methods 2026-04-15 08:55:00 +05:30
Chirag Aggarwal 54d6470163 refactor: move acquire/release into ResolverLock as instance methods 2026-04-15 08:51:14 +05:30
Chirag Aggarwal 50640b5bb9 refactor: inline createResolverResponse — only called once 2026-04-15 08:50:14 +05:30
Chirag Aggarwal 4d4ba508ef fix: use spl_object_hash for lock keys instead of WeakMap
Replace WeakMap with a plain array keyed by spl_object_hash($utopia)
as suggested in review. Entry is cleaned up in the finally block to
prevent leaks.
2026-04-15 08:47:41 +05:30
Chirag Aggarwal 4f2f9fedfa fix: resolve merge conflict keeping both lock and route restore
Merge conflict in Resolvers.php between the coroutine lock
(fix/graphql-coroutine-safe-response) and the otel route restore
(fix-gql-route-reset from 1.9.x). Both changes are needed:
the lock serialises concurrent resolvers while the route restore
prevents otel span clobbering.
2026-04-15 08:40:12 +05:30
Matej Bačo 732538504d Fix backwards compatibility 2026-04-14 17:42:26 +02:00
Matej Bačo d3dcbfe567 Leftover chaneges, 2026-04-14 17:20:22 +02:00
Matej Bačo 905d2a8eaa Fix tests 2026-04-14 17:20:03 +02:00
Matej Bačo b5e46c1a60 E2E integration tests 2026-04-14 17:09:37 +02:00
Matej Bačo a3a8ad88fe formatting fix 2026-04-14 17:07:06 +02:00
Matej Bačo 9d6428d5d5 Improve tests 2026-04-14 17:06:57 +02:00
Matej Bačo d7f8ca3f01 Improve endpoint quality 2026-04-14 17:04:22 +02:00
Matej Bačo 56bcc0d09f Fix tests 2026-04-14 17:01:56 +02:00
Matej Bačo 8b0c60d2f9 Fix bugs 2026-04-14 17:01:46 +02:00
Matej Bačo ffdfe4bbf8 Add new tests 2026-04-14 16:55:39 +02:00
Matej Bačo 556ca10ed9 Register endpoints 2026-04-14 16:52:38 +02:00
Matej Bačo 193beb76fe add SMTP endpoints 2026-04-14 16:50:07 +02:00
Chirag AggarwalandGitHub 8fcba8b5c9 Merge pull request #11893 from appwrite/codex/disable-graphql-functional 2026-04-14 19:19:14 +05:30
Jake BarnbyandGitHub 6c8b34c230 Merge pull request #11885 from appwrite/fix-gql-route-reset
(fix): reset route to avoid clobbering otel
2026-04-15 01:26:02 +12:00
Chirag Aggarwal b2884ddb88 Use audit message context helper 2026-04-14 18:23:24 +05:30
Chirag Aggarwal 82798fa5a3 Simplify audit message construction 2026-04-14 18:18:25 +05:30
Shimon NewmanandGitHub 377ade3c44 Merge pull request #11892 from appwrite/schedule-functions-debug-2
Schedule functions debug
2026-04-14 15:32:29 +03:00
Shimon NewmanGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
71533aaaf3 Update app/cli.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-14 15:17:37 +03:00
shimon ccbe7e5d3f Merge branch '1.9.x' of github.com:appwrite/appwrite into schedule-functions-debug-2 2026-04-14 15:11:56 +03:00
Chirag AggarwalandGitHub bea7739d7e Merge branch '1.9.x' into codex/disable-graphql-functional 2026-04-14 17:21:55 +05:30
Jake BarnbyandGitHub 76320d652a Merge pull request #11887 from appwrite/fix/composer-audit-graphql-php
Upgrade graphql-php to v15.31.5
2026-04-14 23:46:45 +12:00
Chirag Aggarwal 6d5968e2ea ci: disable functional GraphQL e2e 2026-04-14 16:51:46 +05:30
Jake Barnby fd8fedca18 (refactor): Remove shared tables V1/V2 versioning 2026-04-14 22:51:36 +12:00
shimon 70c380fa36 feat: add tracing support for execution events in log and worker classes 2026-04-14 13:36:29 +03:00
shimon 512a7ae2bd feat: enhance function scheduling with tracing support 2026-04-14 13:36:19 +03:00
Chirag AggarwalandGitHub 49d3cd980f Merge branch '1.9.x' into fix/composer-audit-graphql-php 2026-04-14 15:07:22 +05:30
Luke B. SilverandGitHub 39a396810a Merge pull request #11881 from appwrite/feat/45-min-build-timeout
feat: increase default build timeout to 45 minutes
2026-04-14 09:28:29 +01:00
Jake BarnbyandGitHub c52ca65905 Revert "Safe delete shared tables v1" 2026-04-14 19:15:41 +12:00
Jake BarnbyandGitHub 1d307178bb Merge pull request #11691 from appwrite/delete-project-shared-table-v1
Safe delete shared tables v1
2026-04-14 18:00:00 +12:00
fogelito c3ed2ff6ce Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1 2026-04-14 08:05:44 +03:00
Chirag Aggarwal efadf17bfe Fix GraphQL 15 static analysis 2026-04-14 10:26:59 +05:30
Chirag Aggarwal bcfec8d5de Align graphql version pinning style 2026-04-14 09:35:25 +05:30
Chirag Aggarwal 4b2e22d9da Fix graphql-php audit vulnerability 2026-04-14 09:27:14 +05:30
ArnabChatterjee20k 45e22e2243 Merge remote-tracking branch 'origin/1.9.x' into docsdb-fixes 2026-04-13 22:44:07 +05:30
Matej BačoandGitHub 0a21f3b139 Merge pull request #11884 from appwrite/fix-webhook-api-security
Fix: Webhook API secret rotation and security
2026-04-13 15:52:55 +02:00
Chirag AggarwalandGitHub 5d53685976 Merge branch '1.9.x' into chore-migrate-audits-certificates-screenshots-to-publishers 2026-04-13 19:11:43 +05:30
Chirag AggarwalandGitHub d869c39783 Merge branch '1.9.x' into fix/graphql-coroutine-safe-response 2026-04-13 19:02:06 +05:30
Chirag Aggarwal fe02964ebd fix: finalize graphql coroutine response isolation 2026-04-13 19:01:20 +05:30
Jake Barnby cc8eb62c83 (chore): rename 2026-04-14 01:15:06 +12:00
Luke B. SilverandGitHub b1ad6ea87c Merge pull request #11883 from appwrite/fix/large-execution-payload
fix: trim execution queue payload to project ID only
2026-04-13 14:13:43 +01:00
Jake Barnby d52d6c0bf0 (fix): reset route to avoid clobbering otel 2026-04-14 01:13:32 +12:00
Chirag AggarwalandGitHub 86cfea0edb Merge branch '1.9.x' into chore-migrate-audits-certificates-screenshots-to-publishers 2026-04-13 18:41:52 +05:30
Chirag Aggarwal a1342b4b9d fix: update audit context usage 2026-04-13 18:32:38 +05:30
loks0nandClaude Sonnet 4.6 a941d8b855 fix: trim execution queue payload to project ID only
The v1-executions queue was serialising the full project document
(OAuth providers, webhooks, keys, auths config, permissions, etc.)
into every message. The worker DI system already re-fetches the
complete project from the platform database using only the project ID,
so the full document was wasted bytes.

Override trimPayload() in the Execution event class to include only
$id in the project stub, reducing message size significantly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 13:52:09 +01:00
Chirag Aggarwal d40a355d9d refactor: simplify audit event context 2026-04-13 18:21:39 +05:30
Chirag Aggarwal f4f5494b85 fix: isolate graphql resolver responses 2026-04-13 17:48:48 +05:30
Chirag Aggarwal fc0fd2f6ac fix: scope graphql resources to resolver coroutine 2026-04-13 17:10:38 +05:30
Chirag Aggarwal 70a75c2e7b fix: scope graphql resolver lock to request 2026-04-13 16:47:33 +05:30
Chirag Aggarwal 6bc2168e29 fix: isolate graphql resolver request state 2026-04-13 16:34:27 +05:30
fogelito a3ba66c90a Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1
# Conflicts:
#	composer.lock
2026-04-13 13:29:03 +03:00
Luke B. SilverGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
df5ccc10ad Apply suggestion from @greptile-apps[bot]
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-13 10:48:20 +01:00
loks0nandClaude Sonnet 4.6 2807d6cd9a feat: increase default build timeout to 45 minutes
Raises _APP_COMPUTE_BUILD_TIMEOUT default from 900s (15 min) to
2700s (45 min) to support longer-running builds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 09:57:27 +01:00
Damodar LohaniandClaude Opus 4.6 18b1c344e6 fix: serialize batched GraphQL queries for coroutine safety
When Swoole coroutine hooks are enabled (SWOOLE_HOOK_ALL), batched
GraphQL queries execute in parallel coroutines that share a single
Response object. Concurrent coroutines interleave writes to the
shared response payload, causing data mixing between queries.

Cloning the response is not viable because cookies/headers written
by the action (e.g. session tokens) must reach the real HTTP response.

Instead, serialize the critical section (execute → getPayload) using
a Swoole Channel as a coroutine-safe mutex. This ensures only one
batched query writes to the Response at a time while preserving
cookie/header propagation. The lock is released before resolve/reject
callbacks so downstream processing remains concurrent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 06:54:15 +00:00
Damodar LohaniandGitHub c6e32940f4 Merge branch '1.9.x' into CLO-4175-allow-delete-with-memberships 2026-04-13 07:21:38 +05:45
Chirag AggarwalandGitHub a31cb67ec5 Merge branch '1.9.x' into chore-migrate-audits-certificates-screenshots-to-publishers 2026-04-11 22:56:10 +05:30
Chirag Aggarwal 5ecd15a5f5 fix: register certificate publisher in cli 2026-04-11 09:07:51 +05:30
Chirag Aggarwal ec5472f1ed chore: remove unrelated queue resources 2026-04-11 08:57:06 +05:30
Chirag AggarwalandGitHub 9ae804f8ae Merge branch '1.9.x' into chore-migrate-audits-certificates-screenshots-to-publishers 2026-04-11 08:49:23 +05:30
ArnabChatterjee20k 0cce480592 typo 2026-04-10 17:23:43 +05:30
ArnabChatterjee20k b622b092a8 updated 2026-04-10 17:22:22 +05:30
ArnabChatterjee20k 96fe989f6d update composer dependencies and remove obsolete log classes 2026-04-10 17:15:31 +05:30
Chirag Aggarwal 0de26be6e6 chore: address review feedback 2026-04-10 16:44:24 +05:30
Chirag Aggarwal dc0a5c88b7 refactor: migrate audits certificates screenshots to publishers 2026-04-10 16:44:00 +05:30
ArnabChatterjee20k d13dbae0fe added allowance of empty payload for documentsdb 2026-04-10 13:59:45 +05:30
Damodar LohaniandGitHub d6f51a96a5 Merge branch '1.9.x' into CLO-4175-allow-delete-with-memberships 2026-04-09 07:32:30 +05:45
Damodar LohaniandGitHub 8442a1e612 Merge branch '1.9.x' into CLO-4175-allow-delete-with-memberships 2026-04-07 06:27:57 +05:45
fogelito 31728c9b72 Update lock 2026-04-06 09:29:58 +03:00
fogelito 9d0fc9e5b1 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1 2026-04-06 09:19:38 +03:00
fogelito a332eb5f32 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1 2026-04-05 11:42:25 +03:00
Damodar LohaniandClaude Opus 4.6 cc82b1a5cf fix: don't promote non-owners on account deletion, leave team orphaned instead
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 07:15:35 +00:00
Damodar LohaniandClaude Opus 4.6 ba32012744 fix: filter unconfirmed members from owner count, ownership transfer, and primary user transfer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 07:11:32 +00:00
fogelito 775d21e5ee lock 2026-04-05 10:08:29 +03:00
Damodar LohaniandClaude Opus 4.6 8f6530d1e7 fix: remove unused $userInternalId from closure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 05:34:04 +00:00
Damodar LohaniandClaude Opus 4.6 4297c70f58 fix: address review feedback — safer orphan approach, veteran ordering, deduplicate transfer
- Remove team deletion for sole owner+sole member case; let orphan teams
  be cleaned up by Cloud's inactive project cleanup (safer, avoids
  accidental data loss)
- Add explicit ordering by $createdAt so the most veteran member gets
  ownership transfer, with limit(1) for clarity
- Remove confirm filter on primary user transfer in membership deletion
  so all members (including unconfirmed) are considered
- Remove redundant ownership transfer from Deletes worker since the API
  controller already handles it before queueing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 05:22:02 +00:00
Damodar LohaniandClaude Opus 4.6 16ed60a5c3 Filter unconfirmed members when transferring team ownership
Prevent unconfirmed (pending invite) members from being promoted to
owner or set as the team's primary user during membership/account
deletion by adding a Query::equal('confirm', [true]) filter to the
relevant findOne queries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 02:02:09 +00:00
Damodar LohaniandClaude Opus 4.6 d831b93934 Allow deleting user account with active memberships
Instead of blocking account deletion when the user has confirmed team
memberships, handle memberships gracefully during deletion:

- Sole owner + sole member: delete the team and queue project cleanup
- Sole owner + other members: transfer ownership to the next member
- Non-owner / multiple owners: no special handling needed (worker cleans up)

Also update the Deletes worker to transfer the team's primary user
reference when removing a deleted user's memberships.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:43:05 +00:00
fogelito e42a26fdad comment 2026-03-31 19:20:13 +03:00
fogelito 2c4770d29b Database 5.3.19 2026-03-31 19:18:32 +03:00
fogelito 2a9e423cb1 Temporary disabling deletes from internal collections 2026-03-31 12:54:33 +03:00
fogelito 14869cc6d6 Safe delete shared tables v1 2026-03-31 09:59:53 +03:00
fogelito a46fc5e153 Remove var_dump 2025-10-16 17:28:25 +03:00
fogelito 08ec06dfe5 linter 2025-10-16 17:13:42 +03:00
fogelito 46e6e34c8a Remove var_dumps 2025-10-16 17:09:46 +03:00
fogelito 0d95ba99d3 Add DOCUMENT_UNIQUE_VIOLATION 2025-10-16 16:59:02 +03:00
fogelito effd180e0b linter 2025-10-16 13:58:54 +03:00
fogelito 57f4ae7395 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into unique-exception 2025-10-16 13:56:58 +03:00
fogelito 4d91f1a1b8 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into unique-exception
# Conflicts:
#	composer.lock
2025-10-16 12:43:39 +03:00
fogelito d3dc178c27 Run tests 2025-10-16 12:38:49 +03:00
532 changed files with 27445 additions and 4130 deletions
+2
View File
@@ -146,3 +146,5 @@ _APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main
_APP_TRUSTED_HEADERS=x-forwarded-for
_APP_POOL_ADAPTER=stack
_APP_WORKER_SCREENSHOTS_ROUTER=http://appwrite
_TESTS_OAUTH2_GITHUB_CLIENT_ID=
_TESTS_OAUTH2_GITHUB_CLIENT_SECRET=
+349
View File
@@ -0,0 +1,349 @@
const fs = require('fs');
const marker = '<!-- appwrite-benchmark-results -->';
const serviceLabels = ['Account', 'TablesDB', 'Storage', 'Functions'];
module.exports = async ({ github, context, core }) => {
const body = buildComment(core);
fs.writeFileSync('benchmark-comment.txt', body);
const pullRequest = context.payload.pull_request;
if (!pullRequest || pullRequest.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) {
return;
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
per_page: 100,
});
const existing = comments.find((comment) => {
return comment.user?.type === 'Bot' && comment.body?.includes(marker);
}) || comments.find((comment) => {
return comment.user?.type === 'Bot' && comment.body?.includes('Benchmark results');
});
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
return;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
body,
});
};
function buildComment(core) {
const before = readSummary('benchmark-before-summary.json', core);
const after = readSummary('benchmark-after-summary.json', core);
const beforeSamples = readSamples('benchmark-before-samples.json', core);
const afterSamples = readSamples('benchmark-after-samples.json', core);
const baseRef = markdownText(process.env.BENCHMARK_BASE_REF || 'base');
const headRef = markdownText(process.env.BENCHMARK_HEAD_REF || 'head');
const rows = benchmarkRows(before, after, beforeSamples, afterSamples);
const topWaits = topSamples(afterSamples, 'appwrite_api_waiting', 3);
const lines = [
marker,
'## :sparkles: Benchmark results',
'',
`Comparing ${baseRef} (before) to ${headRef} (after).`,
'',
];
if (before === null) {
lines.push('> Before benchmark did not complete; showing current branch metrics only.', '');
}
if (after === null) {
lines.push('> Current branch benchmark did not complete; showing available metrics only.', '');
}
lines.push(
'**Before**',
'',
metricTable(rows, 'before'),
'',
'**After**',
'',
metricTable(rows, 'after'),
'',
'**Delta**',
'',
'| Scenario | P95 delta (ms) |',
'| --- | ---: |',
...rows.map(deltaRow),
'',
'<details>',
'<summary><strong>Top API waits</strong></summary>',
'',
'<br>',
'',
'| API request | Max wait (ms) |',
'| --- | ---: |',
...topWaitRows(topWaits),
'',
'</details>',
);
return `${lines.join('\n')}\n`;
}
function readSummary(path, core) {
if (!fs.existsSync(path)) {
return null;
}
try {
return JSON.parse(fs.readFileSync(path, 'utf8'));
} catch (error) {
core?.warning(`Invalid benchmark summary ${path}: ${error.message}`);
return null;
}
}
function readSamples(path, core) {
if (!fs.existsSync(path)) {
return [];
}
const contents = fs.readFileSync(path, 'utf8').trim();
if (contents === '') {
return [];
}
return contents
.split('\n')
.filter(Boolean)
.flatMap((line) => {
try {
return [JSON.parse(line)];
} catch (error) {
core?.warning(`Invalid benchmark sample in ${path}: ${error.message}`);
return [];
}
});
}
function benchmarkRows(before, after, beforeSamples, afterSamples) {
const beforeServices = serviceStats(beforeSamples);
const afterServices = serviceStats(afterSamples);
return [
{
label: 'API total',
before: apiSampleStats(beforeSamples) || summaryStats(before, 'appwrite_api_duration'),
after: apiSampleStats(afterSamples) || summaryStats(after, 'appwrite_api_duration'),
},
...serviceLabels.map((label) => ({
label,
before: beforeServices.get(label) || null,
after: afterServices.get(label) || null,
})),
];
}
function summaryStats(summary, durationMetric, iterationsMetric = null, rpsMetric = null) {
const values = metricValues(summary, durationMetric);
if (!values) {
return null;
}
return {
p50: values.med ?? null,
p95: values['p(95)'] ?? null,
iterations: iterationsMetric ? metricValue(summary, iterationsMetric, 'count') : values.count ?? null,
rps: rpsMetric ? metricValue(summary, rpsMetric, 'rate') : null,
};
}
function serviceStats(samples) {
const apiSamples = samples.filter((sample) => {
return sample.metric === 'appwrite_api_duration' && typeof sample.data?.value === 'number';
});
const groups = new Map();
for (const sample of apiSamples) {
const service = serviceFromName(sample.data.tags?.name || '');
if (!service) {
continue;
}
const serviceSamples = groups.get(service) || [];
serviceSamples.push(sample);
groups.set(service, serviceSamples);
}
return new Map([...groups.entries()].map(([service, serviceSamples]) => {
const values = serviceSamples.map((sample) => sample.data.value);
const durationSeconds = sampleWindowSeconds(serviceSamples);
return [service, {
p50: percentile(values, 50),
p95: percentile(values, 95),
iterations: values.length,
rps: durationSeconds ? values.length / durationSeconds : null,
}];
}));
}
function apiSampleStats(samples) {
const apiSamples = samples.filter((sample) => {
return sample.metric === 'appwrite_api_duration' && typeof sample.data?.value === 'number';
});
const values = apiSamples.map((sample) => sample.data.value);
if (values.length === 0) {
return null;
}
const durationSeconds = sampleWindowSeconds(apiSamples);
return {
p50: percentile(values, 50),
p95: percentile(values, 95),
iterations: values.length,
rps: durationSeconds ? values.length / durationSeconds : null,
};
}
function serviceFromName(name) {
if (name.startsWith('account.')) {
return 'Account';
}
if (name.startsWith('tablesdb.')) {
return 'TablesDB';
}
if (name.startsWith('storage.') || name.startsWith('tokens.')) {
return 'Storage';
}
if (name.startsWith('functions.')) {
return 'Functions';
}
return null;
}
function sampleWindowSeconds(samples) {
const times = samples
.map((sample) => Date.parse(sample.data?.time))
.filter((value) => !Number.isNaN(value));
if (times.length < 2) {
return null;
}
return Math.max((Math.max(...times) - Math.min(...times)) / 1000, 1);
}
function percentile(values, percentileValue) {
if (values.length === 0) {
return null;
}
const sorted = [...values].sort((left, right) => left - right);
const index = Math.ceil((percentileValue / 100) * sorted.length) - 1;
return sorted[Math.max(0, Math.min(index, sorted.length - 1))];
}
function metricValues(data, metric) {
return data?.metrics?.[metric]?.values ?? null;
}
function metricValue(data, metric, stat) {
return metricValues(data, metric)?.[stat] ?? null;
}
function metricTable(rows, side) {
return [
'| Scenario | P50 (ms) | P95 (ms) | Requests | RPS |',
'| --- | ---: | ---: | ---: | ---: |',
...rows.map((row) => metricRow(row, side)),
].join('\n');
}
function metricRow(row, side) {
const values = row[side];
return `| ${row.label} | ${formatMs(values?.p50)} | ${formatMs(values?.p95)} | ${formatCount(values?.iterations)} | ${formatRate(values?.rps)} |`;
}
function deltaRow(row) {
return `| ${row.label} | ${formatDelta(row.before?.p95, row.after?.p95)} |`;
}
function topSamples(samples, metric, limit) {
const byName = samples.reduce((result, sample) => {
if (sample.metric !== metric || typeof sample.data?.value !== 'number') {
return result;
}
const name = sample.data.tags?.name || 'unknown';
const current = result.get(name);
if (!current || sample.data.value > current.value) {
result.set(name, { name, value: sample.data.value });
}
return result;
}, new Map());
return [...byName.values()]
.sort((left, right) => right.value - left.value)
.slice(0, limit);
}
function topWaitRows(samples) {
if (samples.length === 0) {
return ['| n/a | n/a |'];
}
return samples.map((sample) => {
return `| ${markdownText(sample.name).replace(/\|/g, '\\|')} | ${formatMs(sample.value)} |`;
});
}
function markdownText(value) {
return String(value || '').replace(/[\r\n]/g, ' ').replace(/[&<>"']/g, (char) => {
return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' })[char];
});
}
function formatMs(value) {
return formatNumber(value, 2);
}
function formatRate(value) {
return formatNumber(value, 2);
}
function formatCount(value) {
if (value === null || value === undefined || Number.isNaN(value)) {
return 'n/a';
}
return `${Math.round(value)}`;
}
function formatDelta(before, after) {
if (before === null || before === undefined || after === null || after === undefined || Number.isNaN(before) || Number.isNaN(after)) {
return 'n/a';
}
const difference = Number((after - before).toFixed(2));
return `${difference > 0 ? '+' : ''}${trimNumber(difference)}`;
}
function formatNumber(value, decimals) {
if (value === null || value === undefined || Number.isNaN(value)) {
return 'n/a';
}
return trimNumber(Number(value).toFixed(decimals));
}
function trimNumber(value) {
const text = String(value);
const trimmed = text.includes('.') ? text.replace(/\.?0+$/, '') : text;
return trimmed === '' ? '0' : trimmed;
}
+155 -74
View File
@@ -7,6 +7,7 @@ concurrency:
env:
COMPOSE_FILE: docker-compose.yml
IMAGE: appwrite-dev
K6_VERSION: '0.53.0'
on:
pull_request:
@@ -210,7 +211,7 @@ jobs:
with:
script: |
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
const allModes = ['dedicated', 'shared_v1', 'shared_v2'];
const allModes = ['dedicated', 'shared'];
const defaultDatabases = ['MongoDB'];
const defaultModes = ['dedicated'];
@@ -429,6 +430,8 @@ jobs:
include:
- service: Databases
runner: blacksmith-4vcpu-ubuntu-2404
paratest_processes: 3
timeout_minutes: 30
- service: Sites
runner: blacksmith-4vcpu-ubuntu-2404
- service: Functions
@@ -439,6 +442,10 @@ jobs:
runner: blacksmith-4vcpu-ubuntu-2404
- service: TablesDB
runner: blacksmith-4vcpu-ubuntu-2404
paratest_processes: 3
timeout_minutes: 30
- service: Migrations
paratest_processes: 1
steps:
- name: Checkout repository
uses: actions/checkout@v6
@@ -449,8 +456,10 @@ jobs:
name: ${{ env.IMAGE }}
path: /tmp
- name: Set database environment
- name: Set environment
run: |
echo "_APP_OPTIONS_ROUTER_PROTECTION=enabled" >> $GITHUB_ENV
if [ "${{ matrix.database }}" = "MariaDB" ]; then
echo "COMPOSE_PROFILES=mariadb" >> $GITHUB_ENV
echo "_APP_DB_ADAPTER=mariadb" >> $GITHUB_ENV
@@ -479,11 +488,8 @@ jobs:
env:
_APP_BROWSER_HOST: http://invalid-browser/v1
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
@@ -502,7 +508,7 @@ jobs:
with:
max_attempts: 2
retry_wait_seconds: 60
timeout_minutes: 20
timeout_minutes: ${{ matrix.timeout_minutes || 20 }}
job_id: ${{ job.check_run_id }}
github_token: ${{ secrets.GITHUB_TOKEN }}
test_dir: tests/e2e/Services/${{ matrix.service }}
@@ -512,12 +518,19 @@ jobs:
# Services that rely on sequential test method execution (shared static state)
FUNCTIONAL_FLAG="--functional"
case "${{ matrix.service }}" in
Databases|TablesDB|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
Databases|TablesDB|Functions|Realtime|GraphQL|ProjectWebhooks) FUNCTIONAL_FLAG="" ;;
esac
PARATEST_PROCESSES="${{ matrix.paratest_processes }}"
if [ -z "$PARATEST_PROCESSES" ]; then
PARATEST_PROCESSES="$(nproc)"
fi
docker compose exec -T \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite vendor/bin/paratest --processes $(nproc) $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
-e _TESTS_OAUTH2_GITHUB_CLIENT_ID="${{ secrets.TESTS_OAUTH2_GITHUB_CLIENT_ID }}" \
-e _TESTS_OAUTH2_GITHUB_CLIENT_SECRET="${{ secrets.TESTS_OAUTH2_GITHUB_CLIENT_SECRET }}" \
appwrite vendor/bin/paratest --processes "$PARATEST_PROCESSES" $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
- name: Failure Logs
if: failure()
@@ -539,6 +552,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Download Docker Image
uses: actions/download-artifact@v7
@@ -557,11 +572,8 @@ jobs:
env:
_APP_OPTIONS_ABUSE: enabled
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
@@ -618,11 +630,8 @@ jobs:
timeout-minutes: 5
env:
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
@@ -658,13 +667,19 @@ jobs:
benchmark:
name: Benchmark
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
needs: build
permissions:
actions: read
contents: read
issues: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Download Docker Image
uses: actions/download-artifact@v7
@@ -678,79 +693,145 @@ jobs:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Load and Start Appwrite
- name: Load Appwrite image
run: |
sed -i 's/traefik/localhost/g' .env
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose up -d
sleep 10
docker tag ${{ env.IMAGE }} ${{ env.IMAGE }}:after
- name: Install Oha
- name: Setup k6
uses: grafana/setup-k6-action@ffe7d7290dfa715e48c2ccc924d068444c94bde2
with:
k6-version: ${{ env.K6_VERSION }}
- name: Prepare benchmark before
id: benchmark_before_prepare
continue-on-error: true
run: |
echo "deb [signed-by=/usr/share/keyrings/azlux-archive-keyring.gpg] http://packages.azlux.fr/debian/ stable main" | sudo tee /etc/apt/sources.list.d/azlux.list
sudo wget -O /usr/share/keyrings/azlux-archive-keyring.gpg https://azlux.fr/repo.gpg
sudo apt update
sudo apt install oha
oha --version
git fetch --depth=1 origin ${{ github.event.pull_request.base.sha }}
git worktree add --detach /tmp/appwrite-benchmark-before ${{ github.event.pull_request.base.sha }}
docker build \
--cache-from ${{ env.IMAGE }}:after \
--target development \
--build-arg DEBUG=false \
--build-arg TESTING=true \
--build-arg VERSION=dev \
--tag ${{ env.IMAGE }}:before \
/tmp/appwrite-benchmark-before
- name: Benchmark PR
run: 'oha -z 180s http://localhost/v1/health/version --output-format json > benchmark.json'
- name: Cleaning
run: docker compose down -v
- name: Installing latest version
- name: Start before Appwrite
id: benchmark_before_start
if: steps.benchmark_before_prepare.outcome == 'success'
continue-on-error: true
working-directory: /tmp/appwrite-benchmark-before
env:
_APP_DOMAIN: localhost
_APP_CONSOLE_DOMAIN: localhost
_APP_DOMAIN_FUNCTIONS: functions.localhost
_APP_OPTIONS_ABUSE: disabled
run: |
rm docker-compose.yml
rm .env
curl https://appwrite.io/install/compose -o docker-compose.yml
curl https://appwrite.io/install/env -o .env
sed -i 's/_APP_OPTIONS_ABUSE=enabled/_APP_OPTIONS_ABUSE=disabled/g' .env
docker compose up -d
sleep 10
docker tag ${{ env.IMAGE }}:before ${{ env.IMAGE }}
docker compose up -d --wait --no-build
- name: Benchmark Latest
run: oha -z 180s http://localhost/v1/health/version --output-format json > benchmark-latest.json
- name: Prepare benchmark files
run: rm -f benchmark-before-summary.json benchmark-after-summary.json benchmark-before-samples.json benchmark-after-samples.json
- name: Prepare comment
- name: Benchmark before
if: steps.benchmark_before_start.outcome == 'success'
continue-on-error: true
uses: grafana/run-k6-action@a15e2072ede004e8d46141e33d7f7dad8ad08d9d
env:
APPWRITE_ENDPOINT: 'http://localhost/v1'
APPWRITE_BENCHMARK_ITERATIONS: '5'
APPWRITE_BENCHMARK_VUS: '1'
APPWRITE_WORKER_TIMEOUT_MS: '120000'
APPWRITE_BENCHMARK_SUMMARY_PATH: 'benchmark-before-summary.json'
with:
path: tests/benchmarks/http.js
flags: --quiet --out json=benchmark-before-samples.json
cloud-comment-on-pr: false
debug: true
- name: Stop before Appwrite
if: always()
run: |
echo '## :sparkles: Benchmark results' > benchmark.txt
echo ' ' >> benchmark.txt
echo "- Requests per second: $(jq -r '.summary.requestsPerSec|tonumber?|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json)" >> benchmark.txt
echo "- Requests with 200 status code: $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json)" >> benchmark.txt
echo "- P99 latency: $(jq -r '.latencyPercentiles.p99' benchmark.json )" >> benchmark.txt
echo " " >> benchmark.txt
echo " " >> benchmark.txt
echo "## :zap: Benchmark Comparison" >> benchmark.txt
echo " " >> benchmark.txt
echo "| Metric | This PR | Latest version | " >> benchmark.txt
echo "| --- | --- | --- | " >> benchmark.txt
echo "| RPS | $(jq -r '.summary.requestsPerSec|tonumber?|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json) | $(jq -r '.summary.requestsPerSec|tonumber|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark-latest.json) | " >> benchmark.txt
echo "| 200 | $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json) | $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark-latest.json) | " >> benchmark.txt
echo "| P99 | $(jq -r '.latencyPercentiles.p99' benchmark.json ) | $(jq -r '.latencyPercentiles.p99' benchmark-latest.json ) | " >> benchmark.txt
if [ -d /tmp/appwrite-benchmark-before ]; then
cd /tmp/appwrite-benchmark-before
docker compose down -v || true
fi
- name: Wait for benchmark ports
if: always()
run: |
for port in 80 443 8080 9503; do
for attempt in $(seq 1 30); do
if ! ss -ltn | awk '{print $4}' | grep -Eq "[:.]${port}$"; then
break
fi
sleep 1
done
if ss -ltn | awk '{print $4}' | grep -Eq "[:.]${port}$"; then
echo "Port ${port} is still in use after stopping the before stack"
ss -ltn
exit 1
fi
done
- name: Start after Appwrite
env:
_APP_DOMAIN: localhost
_APP_CONSOLE_DOMAIN: localhost
_APP_DOMAIN_FUNCTIONS: functions.localhost
_APP_OPTIONS_ABUSE: disabled
run: |
docker tag ${{ env.IMAGE }}:after ${{ env.IMAGE }}
docker compose up -d --wait --no-build
- name: Benchmark after
id: benchmark_after
continue-on-error: true
uses: grafana/run-k6-action@a15e2072ede004e8d46141e33d7f7dad8ad08d9d
env:
APPWRITE_ENDPOINT: 'http://localhost/v1'
APPWRITE_BENCHMARK_ITERATIONS: '5'
APPWRITE_BENCHMARK_VUS: '1'
APPWRITE_WORKER_TIMEOUT_MS: '120000'
APPWRITE_BENCHMARK_PREVIOUS_SUMMARY_PATH: '../../benchmark-before-summary.json'
APPWRITE_BENCHMARK_SUMMARY_PATH: 'benchmark-after-summary.json'
with:
path: tests/benchmarks/http.js
flags: --quiet --out json=benchmark-after-samples.json
cloud-comment-on-pr: false
debug: true
- name: Stop after Appwrite
if: always()
run: docker compose down -v || true
- name: Comment on PR
if: always()
uses: actions/github-script@v8
env:
BENCHMARK_BASE_REF: ${{ github.event.pull_request.base.ref }}
BENCHMARK_HEAD_REF: ${{ github.event.pull_request.head.ref }}
with:
script: |
const comment = require('./.github/workflows/benchmark-comment.js');
await comment({ github, context, core });
- name: Save results
uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: benchmark.json
path: benchmark.json
name: benchmark-results
path: |
benchmark-comment.txt
benchmark-before-summary.json
benchmark-after-summary.json
benchmark-before-samples.json
benchmark-after-samples.json
retention-days: 7
- name: Find Comment
if: github.event.pull_request.head.repo.full_name == github.repository
uses: peter-evans/find-comment@v3
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: Benchmark results
- name: Comment on PR
if: github.event.pull_request.head.repo.full_name == github.repository
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-path: benchmark.txt
edit-mode: replace
- name: Fail benchmark
if: always() && steps.benchmark_after.outcome != 'success'
run: exit 1
+1 -1
View File
@@ -892,7 +892,7 @@
* Unset index length by @fogelito in https://github.com/appwrite/appwrite/pull/8978
* Update base to 0.9.5 by @basert in https://github.com/appwrite/appwrite/pull/9005
* Sync main into 1.6.x by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/9011
* Improved shared tables V2 by @abnegate in https://github.com/appwrite/appwrite/pull/9013
* Improved shared tables by @abnegate in https://github.com/appwrite/appwrite/pull/9013
* Ensure backwards compatibility for 1.6.x by @christyjacob4 in https://github.com/appwrite/appwrite/pull/9018
# Version 1.6.0
+3 -3
View File
@@ -72,7 +72,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.9.1
appwrite/appwrite:1.9.0
```
### Windows
@@ -84,7 +84,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.9.1
appwrite/appwrite:1.9.0
```
#### PowerShell
@@ -94,7 +94,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.9.1
appwrite/appwrite:1.9.0
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
+3 -3
View File
@@ -75,7 +75,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.9.1
appwrite/appwrite:1.9.0
```
### Windows
@@ -88,7 +88,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.9.1
appwrite/appwrite:1.9.0
```
#### PowerShell
@@ -99,7 +99,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.9.1
appwrite/appwrite:1.9.0
```
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
+5 -4
View File
@@ -2,10 +2,10 @@
require_once __DIR__ . '/init.php';
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Platform\Appwrite;
@@ -253,6 +253,10 @@ $container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePubli
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
$publisher,
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForStatsResources', fn (Publisher $publisher) => new StatsResourcesPublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
@@ -263,9 +267,6 @@ $container->set('queueForFunctions', function (Publisher $publisher) {
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$container->set('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
$container->set('logError', function (Registry $register) {
return function (Throwable $error, string $namespace, string $action) use ($register) {
Console::error('[Error] Timestamp: ' . date('c', time()));
+5
View File
@@ -34,6 +34,11 @@ $console = [
'legalAddress' => '',
'legalTaxId' => '',
'auths' => [
'membershipsUserName' => true,
'membershipsUserEmail' => true,
'membershipsMfa' => true,
'membershipsUserId' => true,
'membershipsUserPhone' => true,
'mockNumbers' => [],
'invites' => System::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled',
'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user
+25
View File
@@ -797,6 +797,11 @@ return [
'description' => 'Document with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::DOCUMENT_UNIQUE_VIOLATION => [
'name' => Exception::DOCUMENT_UNIQUE_VIOLATION,
'description' => 'Document unique constraint violation. Please try again with different data.',
'code' => 409,
],
Exception::DOCUMENT_UPDATE_CONFLICT => [
'name' => Exception::DOCUMENT_UPDATE_CONFLICT,
'description' => 'Remote document is newer than local.',
@@ -834,6 +839,11 @@ return [
'description' => 'Row with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::ROW_UNIQUE_VIOLATION => [
'name' => Exception::ROW_UNIQUE_VIOLATION,
'description' => 'Row unique constraint violation. Please try again with different data.',
'code' => 409,
],
Exception::ROW_UPDATE_CONFLICT => [
'name' => Exception::ROW_UPDATE_CONFLICT,
'description' => 'Remote row is newer than local.',
@@ -1408,4 +1418,19 @@ return [
'description' => 'When using project API key, make sure to pass x-appwrite-project header with your project ID.',
'code' => 403,
],
Exception::MOCK_NUMBER_ALREADY_EXISTS => [
'name' => Exception::MOCK_NUMBER_ALREADY_EXISTS,
'description' => 'Mock number with the requested number already exists. Try again with a different number. or update OTP of existing mock number.',
'code' => 409,
],
Exception::MOCK_NUMBER_NOT_FOUND => [
'name' => Exception::MOCK_NUMBER_NOT_FOUND,
'description' => 'Mock number with the requested number could not be found.',
'code' => 404,
],
Exception::MOCK_NUMBER_LIMIT_EXCEEDED => [
'name' => Exception::MOCK_NUMBER_LIMIT_EXCEEDED,
'description' => 'The maximum number of mock phones for this project has been reached.',
'code' => 400,
],
];
-6
View File
@@ -9,11 +9,5 @@ return [
'mfaChallenge',
'sessionAlert',
'otpSession'
],
'sms' => [
'verification',
'login',
'invitation',
'mfaChallenge'
]
];
+33
View File
@@ -167,6 +167,17 @@ return [
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\Figma',
],
'fusionauth' => [
'name' => 'FusionAuth',
'developers' => 'https://fusionauth.io/docs/',
'icon' => 'icon-fusionauth',
'enabled' => true,
'sandbox' => false,
'form' => 'fusionauth.phtml',
'beta' => false,
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\FusionAuth',
],
'github' => [
'name' => 'GitHub',
'developers' => 'https://developer.github.com/',
@@ -200,6 +211,28 @@ return [
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\Google',
],
'keycloak' => [
'name' => 'Keycloak',
'developers' => 'https://www.keycloak.org/documentation',
'icon' => 'icon-keycloak',
'enabled' => true,
'sandbox' => false,
'form' => 'keycloak.phtml',
'beta' => false,
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\Keycloak',
],
'kick' => [
'name' => 'Kick',
'developers' => 'https://docs.kick.com/',
'icon' => 'icon-kick',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\Kick',
],
'linkedin' => [
'name' => 'LinkedIn',
'developers' => 'https://developer.linkedin.com/',
+8
View File
@@ -55,6 +55,14 @@ $admins = [
'tables.write',
'platforms.read',
'platforms.write',
'oauth2.read',
'oauth2.write',
'mocks.read',
'mocks.write',
'policies.read',
'policies.write',
'templates.read',
'templates.write',
'projects.write',
'keys.read',
'keys.write',
+32
View File
@@ -204,4 +204,36 @@ return [ // List of publicly visible scopes
"description" =>
"Access to create, update, and delete project\'s platforms",
],
"mocks.read" => [
"description" =>
"Access to read project\'s mocks",
],
"mocks.write" => [
"description" =>
"Access to create, update, and delete project\'s mocks",
],
"policies.read" => [
"description" =>
"Access to read project\'s policies",
],
"policies.write" => [
"description" =>
"Access to update project\'s policies",
],
"templates.read" => [
"description" =>
"Access to read project\'s templates",
],
"templates.write" => [
"description" =>
"Access to create, update, and delete project\'s templates",
],
"oauth2.read" => [
"description" =>
"Access to read project\'s OAuth2 configuration",
],
"oauth2.write" => [
"description" =>
"Access to update project\'s OAuth2 configuration",
],
];
+20
View File
@@ -300,6 +300,26 @@ return [
'repoBranch' => 'main',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/cursor-plugin/CHANGELOG.md'),
],
[
'key' => 'claude-plugin',
'name' => 'ClaudePlugin',
'version' => '0.1.0',
'url' => 'https://github.com/appwrite/claude-plugin.git',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'spec' => 'static',
'family' => APP_SDK_PLATFORM_STATIC,
'prism' => 'claude-plugin',
'source' => \realpath(__DIR__ . '/../sdks/static-claude-plugin'),
'gitUrl' => 'git@github.com:appwrite/claude-plugin.git',
'gitRepoName' => 'claude-plugin',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'repoBranch' => 'main',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/claude-plugin/CHANGELOG.md'),
],
],
],
+6 -6
View File
@@ -1487,13 +1487,13 @@ return [
]
],
[
'key' => 'crm-dashboard-react-admin',
'name' => 'CRM dashboard with React Admin',
'tagline' => 'A React-based admin dashboard template with CRM features.',
'key' => 'dashboard-react-admin',
'name' => 'E-commerce dashboard with React Admin',
'tagline' => 'A React-based admin dashboard template with e-commerce features.',
'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [SiteUseCases::DASHBOARD],
'screenshotDark' => $url . '/images/sites/templates/crm-dashboard-react-admin-dark.png',
'screenshotLight' => $url . '/images/sites/templates/crm-dashboard-react-admin-light.png',
'useCases' => [SiteUseCases::DASHBOARD, SiteUseCases::ECOMMERCE],
'screenshotDark' => $url . '/images/sites/templates/dashboard-react-admin-dark.png',
'screenshotLight' => $url . '/images/sites/templates/dashboard-react-admin-light.png',
'frameworks' => [
getFramework('REACT', [
'providerRootDirectory' => './react/react-admin',
+4 -4
View File
@@ -872,18 +872,18 @@ return [
],
[
'name' => '_APP_FUNCTIONS_BUILD_TIMEOUT',
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 900 seconds.',
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 2700 seconds.',
'introduction' => '0.13.0',
'default' => '900',
'default' => '2700',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_COMPUTE_BUILD_TIMEOUT',
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 900 seconds.',
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 2700 seconds.',
'introduction' => '1.7.0',
'default' => '900',
'default' => '2700',
'required' => false,
'question' => '',
'filter' => ''
+124 -80
View File
@@ -133,9 +133,6 @@ $createSession = function (string $userId, string $secret, Request $request, Res
});
$provider = match ($verifiedToken->getAttribute('type')) {
TOKEN_TYPE_VERIFICATION,
TOKEN_TYPE_RECOVERY,
TOKEN_TYPE_INVITE => SESSION_PROVIDER_EMAIL,
TOKEN_TYPE_MAGIC_URL => SESSION_PROVIDER_MAGIC_URL,
TOKEN_TYPE_PHONE => SESSION_PROVIDER_PHONE,
TOKEN_TYPE_OAUTH2 => $oauthProvider,
@@ -335,15 +332,15 @@ Http::post('/v1/account')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -453,7 +450,7 @@ Http::delete('/v1/account')
->groups(['api', 'account'])
->label('scope', 'account')
->label('audits.event', 'user.delete')
->label('audits.resource', 'user/{response.$id}')
->label('audits.resource', 'user/{user.$id}')
->label('sdk', new Method(
namespace: 'account',
group: 'account',
@@ -474,19 +471,26 @@ Http::delete('/v1/account')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForDeletes')
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes) {
->inject('authorization')
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes, Authorization $authorization) {
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
if ($project->getId() === 'console') {
// get all memberships
$memberships = $user->getAttribute('memberships', []);
foreach ($memberships as $membership) {
// prevent deletion if at least one active membership
if ($membership->getAttribute('confirm', false)) {
throw new Exception(Exception::USER_DELETION_PROHIBITED);
if (!$membership->getAttribute('confirm', false)) {
continue;
}
$team = $dbForProject->getDocument('teams', $membership->getAttribute('teamId'));
if ($team->isEmpty()) {
continue;
}
// Team is left as-is — we don't promote non-owner members to owner.
// Orphan teams are cleaned up later by Cloud's inactive project cleanup.
}
}
@@ -830,7 +834,7 @@ Http::patch('/v1/account/sessions/:sessionId')
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
if (!empty($provider) && $className !== null && \class_exists($className)) {
if (!empty($provider) && \class_exists($className)) {
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
@@ -1597,7 +1601,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
}
}
if ($user === false || $user->isEmpty()) { // No user logged in or with OAuth2 provider ID, create new one or connect with account with same email
if ($user->isEmpty()) { // No user logged in or with OAuth2 provider ID, create new one or connect with account with same email
if (empty($email)) {
$failureRedirect(Exception::USER_UNAUTHORIZED, 'OAuth provider failed to return email.');
}
@@ -1614,7 +1618,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
}
// If user is not found, check if there is a user with the same email
if ($user === false || $user->isEmpty()) {
if ($user->isEmpty()) {
$userWithEmail = $dbForProject->findOne('users', [
Query::equal('email', [$email]),
]);
@@ -1627,7 +1631,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
}
// If user is not found, check if there is an identity with the same email
if ($user === false || $user->isEmpty()) {
if ($user->isEmpty()) {
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
@@ -1639,7 +1643,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
}
}
if ($user === false || $user->isEmpty()) { // Last option -> create the user
if ($user->isEmpty()) { // Last option -> create the user
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
if ($limit !== 0) {
@@ -1672,15 +1676,15 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
$failureRedirect(Exception::USER_EMAIL_FREE);
}
@@ -1813,15 +1817,15 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
$failureRedirect(Exception::USER_EMAIL_FREE);
}
@@ -1947,7 +1951,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
}
if (isset($sessionUpgrade) && $sessionUpgrade && isset($session)) {
if (isset($sessionUpgrade) && isset($session)) {
foreach ($user->getAttribute('targets', []) as $target) {
if ($target->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) {
continue;
@@ -2171,15 +2175,15 @@ Http::post('/v1/account/tokens/magic-url')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -2265,7 +2269,10 @@ Http::post('/v1/account/tokens/magic-url')
$subject = $locale->getText("emails.magicSession.subject");
$preview = $locale->getText("emails.magicSession.preview");
$customTemplate = $project->getAttribute('templates', [])['email.magicSession-' . $locale->default] ?? [];
$customTemplate =
$project->getAttribute('templates', [])['email.magicSession-' . $locale->default] ??
$project->getAttribute('templates', [])['email.magicSession-' . $locale->fallback] ?? [];
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$agentOs = $detector->getOS();
@@ -2295,8 +2302,8 @@ Http::post('/v1/account/tokens/magic-url')
$senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyTo = "";
$replyToEmail = '';
$replyToName = '';
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -2305,8 +2312,13 @@ Http::post('/v1/account/tokens/magic-url')
if (!empty($smtp['senderName'])) {
$senderName = $smtp['senderName'];
}
if (!empty($smtp['replyTo'])) {
$replyTo = $smtp['replyTo'];
// Includes backwards compatibility: fall back to legacy `replyTo` key
$smtpReplyToEmail = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '';
if (!empty($smtpReplyToEmail)) {
$replyToEmail = $smtpReplyToEmail;
}
if (!empty($smtp['replyToName'])) {
$replyToName = $smtp['replyToName'];
}
$queueForMails
@@ -2323,8 +2335,13 @@ Http::post('/v1/account/tokens/magic-url')
if (!empty($customTemplate['senderName'])) {
$senderName = $customTemplate['senderName'];
}
if (!empty($customTemplate['replyTo'])) {
$replyTo = $customTemplate['replyTo'];
// Includes backwards compatibility: fall back to legacy `replyTo` key
$customReplyToEmail = $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? '';
if (!empty($customReplyToEmail)) {
$replyToEmail = $customReplyToEmail;
}
if (!empty($customTemplate['replyToName'])) {
$replyToName = $customTemplate['replyToName'];
}
$body = $customTemplate['message'] ?? '';
@@ -2332,7 +2349,8 @@ Http::post('/v1/account/tokens/magic-url')
}
$queueForMails
->setSmtpReplyTo($replyTo)
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
}
@@ -2478,15 +2496,15 @@ Http::post('/v1/account/tokens/email')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -2575,7 +2593,9 @@ Http::post('/v1/account/tokens/email')
$preview = $locale->getText("emails.otpSession.preview");
$heading = $locale->getText("emails.otpSession.heading");
$customTemplate = $project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ?? [];
$customTemplate =
$project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ??
$project->getAttribute('templates', [])['email.otpSession-' . $locale->fallback] ?? [];
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
$validator = new FileName();
@@ -2611,7 +2631,8 @@ Http::post('/v1/account/tokens/email')
$senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyTo = "";
$replyToEmail = '';
$replyToName = '';
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -2620,8 +2641,13 @@ Http::post('/v1/account/tokens/email')
if (!empty($smtp['senderName'])) {
$senderName = $smtp['senderName'];
}
if (!empty($smtp['replyTo'])) {
$replyTo = $smtp['replyTo'];
// Includes backwards compatibility: fall back to legacy `replyTo` key
$smtpReplyToEmail = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '';
if (!empty($smtpReplyToEmail)) {
$replyToEmail = $smtpReplyToEmail;
}
if (!empty($smtp['replyToName'])) {
$replyToName = $smtp['replyToName'];
}
$queueForMails
@@ -2638,8 +2664,13 @@ Http::post('/v1/account/tokens/email')
if (!empty($customTemplate['senderName'])) {
$senderName = $customTemplate['senderName'];
}
if (!empty($customTemplate['replyTo'])) {
$replyTo = $customTemplate['replyTo'];
// Includes backwards compatibility: fall back to legacy `replyTo` key
$customReplyToEmail = $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? '';
if (!empty($customReplyToEmail)) {
$replyToEmail = $customReplyToEmail;
}
if (!empty($customTemplate['replyToName'])) {
$replyToName = $customTemplate['replyToName'];
}
$body = $customTemplate['message'] ?? '';
@@ -2647,7 +2678,8 @@ Http::post('/v1/account/tokens/email')
}
$queueForMails
->setSmtpReplyTo($replyTo)
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
}
@@ -2968,11 +3000,6 @@ Http::post('/v1/account/tokens/phone')
if ($sendSMS) {
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
$customTemplate = $project->getAttribute('templates', [])['sms.login-' . $locale->default] ?? [];
if (!empty($customTemplate)) {
$message = $customTemplate['message'] ?? $message;
}
$projectName = $project->getAttribute('name');
if ($project->getId() === 'console') {
$projectName = $platform['platformName'];
@@ -3267,7 +3294,7 @@ Http::patch('/v1/account/password')
}
$history[] = $newPassword;
$history = array_slice($history, (count($history) - $historyLimit), $historyLimit);
$history = array_slice($history, -$historyLimit);
}
if ($project->getAttribute('auths', [])['personalDataCheck'] ?? false) {
@@ -3390,15 +3417,15 @@ Http::patch('/v1/account/email')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -3430,9 +3457,6 @@ Http::patch('/v1/account/email')
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldEmail, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
@@ -3519,9 +3543,6 @@ Http::patch('/v1/account/phone')
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
@@ -3726,7 +3747,9 @@ Http::post('/v1/account/recovery')
$body = $locale->getText("emails.recovery.body");
$subject = $locale->getText("emails.recovery.subject");
$preview = $locale->getText("emails.recovery.preview");
$customTemplate = $project->getAttribute('templates', [])['email.recovery-' . $locale->default] ?? [];
$customTemplate =
$project->getAttribute('templates', [])['email.recovery-' . $locale->default] ??
$project->getAttribute('templates', [])['email.recovery-' . $locale->fallback] ?? [];
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
$message
@@ -3743,7 +3766,8 @@ Http::post('/v1/account/recovery')
$senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyTo = "";
$replyToEmail = '';
$replyToName = '';
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -3752,8 +3776,13 @@ Http::post('/v1/account/recovery')
if (!empty($smtp['senderName'])) {
$senderName = $smtp['senderName'];
}
if (!empty($smtp['replyTo'])) {
$replyTo = $smtp['replyTo'];
// Includes backwards compatibility: fall back to legacy `replyTo` key
$smtpReplyToEmail = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '';
if (!empty($smtpReplyToEmail)) {
$replyToEmail = $smtpReplyToEmail;
}
if (!empty($smtp['replyToName'])) {
$replyToName = $smtp['replyToName'];
}
$queueForMails
@@ -3770,8 +3799,13 @@ Http::post('/v1/account/recovery')
if (!empty($customTemplate['senderName'])) {
$senderName = $customTemplate['senderName'];
}
if (!empty($customTemplate['replyTo'])) {
$replyTo = $customTemplate['replyTo'];
// Includes backwards compatibility: fall back to legacy `replyTo` key
$customReplyToEmail = $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? '';
if (!empty($customReplyToEmail)) {
$replyToEmail = $customReplyToEmail;
}
if (!empty($customTemplate['replyToName'])) {
$replyToName = $customTemplate['replyToName'];
}
$body = $customTemplate['message'] ?? '';
@@ -3779,7 +3813,8 @@ Http::post('/v1/account/recovery')
}
$queueForMails
->setSmtpReplyTo($replyTo)
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
}
@@ -4034,7 +4069,9 @@ Http::post('/v1/account/verifications/email')
$subject = $locale->getText("emails.verification.subject");
$heading = $locale->getText("emails.verification.heading");
$customTemplate = $project->getAttribute('templates', [])['email.verification-' . $locale->default] ?? [];
$customTemplate =
$project->getAttribute('templates', [])['email.verification-' . $locale->default] ??
$project->getAttribute('templates', [])['email.verification-' . $locale->fallback] ?? [];
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
$validator = new FileName();
@@ -4060,7 +4097,8 @@ Http::post('/v1/account/verifications/email')
$senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyTo = "";
$replyToEmail = '';
$replyToName = '';
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -4069,8 +4107,13 @@ Http::post('/v1/account/verifications/email')
if (!empty($smtp['senderName'])) {
$senderName = $smtp['senderName'];
}
if (!empty($smtp['replyTo'])) {
$replyTo = $smtp['replyTo'];
// Includes backwards compatibility: fall back to legacy `replyTo` key
$smtpReplyToEmail = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '';
if (!empty($smtpReplyToEmail)) {
$replyToEmail = $smtpReplyToEmail;
}
if (!empty($smtp['replyToName'])) {
$replyToName = $smtp['replyToName'];
}
$queueForMails
@@ -4087,8 +4130,13 @@ Http::post('/v1/account/verifications/email')
if (!empty($customTemplate['senderName'])) {
$senderName = $customTemplate['senderName'];
}
if (!empty($customTemplate['replyTo'])) {
$replyTo = $customTemplate['replyTo'];
// Includes backwards compatibility: fall back to legacy `replyTo` key
$customReplyToEmail = $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? '';
if (!empty($customReplyToEmail)) {
$replyToEmail = $customReplyToEmail;
}
if (!empty($customTemplate['replyToName'])) {
$replyToName = $customTemplate['replyToName'];
}
$body = $customTemplate['message'] ?? '';
@@ -4096,7 +4144,8 @@ Http::post('/v1/account/verifications/email')
}
$queueForMails
->setSmtpReplyTo($replyTo)
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
}
@@ -4333,11 +4382,6 @@ Http::post('/v1/account/verifications/phone')
if ($sendSMS) {
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
$customTemplate = $project->getAttribute('templates', [])['sms.verification-' . $locale->default] ?? [];
if (!empty($customTemplate)) {
$message = $customTemplate['message'] ?? $message;
}
$messageContent = Template::fromString($locale->getText("sms.verification.body"));
$messageContent
->setParam('{{project}}', $project->getAttribute('name'))
@@ -4612,7 +4656,7 @@ Http::delete('/v1/account/targets/:targetId/push')
->groups(['api', 'account'])
->label('scope', 'targets.write')
->label('audits.event', 'target.delete')
->label('audits.resource', 'target/response.$id')
->label('audits.resource', 'target/{request.targetId}')
->label('event', 'users.[userId].targets.[targetId].delete')
->label('sdk', new Method(
namespace: 'account',
+1 -1
View File
@@ -231,7 +231,7 @@ function execute(
$validations = GraphQL::getStandardValidationRules();
if (System::getEnv('_APP_GRAPHQL_INTROSPECTION', 'enabled') === 'disabled') {
$validations[] = new DisableIntrospection();
$validations[] = new DisableIntrospection(DisableIntrospection::ENABLED);
}
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
+1 -14
View File
@@ -482,7 +482,6 @@ Http::post('/v1/messaging/providers/msg91')
$enabled === true
&& \array_key_exists('senderId', $credentials)
&& \array_key_exists('authKey', $credentials)
&& \array_key_exists('from', $options)
) {
$enabled = true;
} else {
@@ -3207,10 +3206,6 @@ Http::post('/v1/messaging/messages/email')
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
}
if ($status === MessageStatus::SCHEDULED && \is_null($scheduledAt)) {
throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE);
}
$mergedTargets = \array_merge($targets, $cc, $bcc);
if (!empty($mergedTargets)) {
@@ -3386,10 +3381,6 @@ Http::post('/v1/messaging/messages/sms')
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
}
if ($status === MessageStatus::SCHEDULED && \is_null($scheduledAt)) {
throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE);
}
if (!empty($targets)) {
$foundTargets = $dbForProject->find('targets', [
Query::equal('$id', $targets),
@@ -3527,10 +3518,6 @@ Http::post('/v1/messaging/messages/push')
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
}
if ($status === MessageStatus::SCHEDULED && \is_null($scheduledAt)) {
throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE);
}
if (!empty($targets)) {
$foundTargets = $dbForProject->find('targets', [
Query::equal('$id', $targets),
@@ -4660,7 +4647,7 @@ Http::delete('/v1/messaging/messages/:messageId')
if (!empty($scheduleId)) {
try {
$dbForPlatform->deleteDocument('schedules', $scheduleId);
} catch (Exception) {
} catch (\Throwable) {
// Ignore
}
}
+2 -1
View File
@@ -51,7 +51,8 @@ function getDatabaseTransferResourceServices(string $databaseType)
DATABASE_TYPE_LEGACY,
DATABASE_TYPE_TABLESDB => Transfer::GROUP_DATABASES_TABLES_DB,
DATABASE_TYPE_VECTORSDB => Transfer::GROUP_DATABASES_VECTOR_DB,
DATABASE_TYPE_DOCUMENTSDB => Transfer::GROUP_DATABASES_DOCUMENTS_DB
DATABASE_TYPE_DOCUMENTSDB => Transfer::GROUP_DATABASES_DOCUMENTS_DB,
default => throw new \LogicException('Unknown database type: ' . $databaseType),
};
}
+2 -1
View File
@@ -113,11 +113,12 @@ Http::get('/v1/project/usage')
$factor = match ($period) {
'1h' => 3600,
'1d' => 86400,
default => throw new \LogicException('Unsupported period: ' . $period),
};
$limit = match ($period) {
'1h' => (new DateTime($startDate))->diff(new DateTime($endDate))->days * 24,
'1d' => (new DateTime($startDate))->diff(new DateTime($endDate))->days
'1d' => (new DateTime($startDate))->diff(new DateTime($endDate))->days,
};
$format = match ($period) {
File diff suppressed because it is too large Load Diff
+5 -10
View File
@@ -1535,7 +1535,7 @@ Http::patch('/v1/users/:userId/email')
Query::equal('identifier', [$email]),
]);
if ($target instanceof Document && !$target->isEmpty()) {
if (!$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
}
@@ -1595,9 +1595,6 @@ Http::patch('/v1/users/:userId/email')
'emailIsDisposable' => $user->getAttribute('emailIsDisposable'),
'emailIsFree' => $user->getAttribute('emailIsFree'),
]));
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldEmail, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
@@ -1681,7 +1678,7 @@ Http::patch('/v1/users/:userId/phone')
Query::equal('identifier', [$number]),
]);
if ($target instanceof Document && !$target->isEmpty()) {
if (!$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
}
@@ -1691,9 +1688,6 @@ Http::patch('/v1/users/:userId/phone')
'phone' => $phoneValue,
'phoneVerification' => $user->getAttribute('phoneVerification'),
]));
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
@@ -2252,8 +2246,8 @@ Http::delete('/v1/users/:userId/mfa/authenticators/:type')
->label('event', 'users.[userId].delete.mfa')
->label('scope', 'users.write')
->label('audits.event', 'user.update')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('audits.resource', 'user/{request.userId}')
->label('audits.userId', '{request.userId}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk', [
new Method(
@@ -2842,6 +2836,7 @@ Http::get('/v1/users/usage')
$format = match ($days['period']) {
'1h' => 'Y-m-d\TH:00:00.000P',
'1d' => 'Y-m-d\T00:00:00.000P',
default => throw new \LogicException('Unsupported period: ' . $days['period']),
};
foreach ($metrics as $metric) {
+48 -42
View File
@@ -7,9 +7,9 @@ use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Network\Cors;
use Appwrite\Platform\Appwrite;
@@ -26,6 +26,7 @@ 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\Request\Filters\V22 as RequestV22;
use Appwrite\Utopia\Request\Filters\V23 as RequestV23;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
@@ -34,6 +35,7 @@ use Appwrite\Utopia\Response\Filters\V19 as ResponseV19;
use Appwrite\Utopia\Response\Filters\V20 as ResponseV20;
use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
use Appwrite\Utopia\Response\Filters\V22 as ResponseV22;
use Appwrite\Utopia\Response\Filters\V23 as ResponseV23;
use Appwrite\Utopia\View;
use Executor\Executor;
use MaxMind\Db\Reader;
@@ -67,7 +69,7 @@ Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
{
$host = $request->getHostname() ?? '';
$host = $request->getHostname();
if (!empty($previewHostname)) {
$host = $previewHostname;
}
@@ -118,7 +120,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
}
if (!in_array($host, $platformHostnames)) {
if (!in_array($host, $platformHostnames) && System::getEnv('_APP_OPTIONS_ROUTER_PROTECTION', 'enabled') === 'enabled') {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.', view: $errorView);
}
@@ -200,12 +202,6 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $activeDeploymentId));
}
if ($deployment->getAttribute('resourceType', '') === 'functions') {
$type = 'function';
} elseif ($deployment->getAttribute('resourceType', '') === 'sites') {
$type = 'site';
}
if ($deployment->isEmpty()) {
$resourceType = $rule->getAttribute('deploymentResourceType', '');
$resourceId = $rule->getAttribute('deploymentResourceId', '');
@@ -215,6 +211,14 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
throw $exception;
}
if ($deployment->getAttribute('resourceType', '') === 'functions') {
$type = 'function';
} elseif ($deployment->getAttribute('resourceType', '') === 'sites') {
$type = 'site';
} else {
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown deployment resource type', view: $errorView);
}
$resource = $type === 'function' ?
$authorization->skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) :
$authorization->skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', '')));
@@ -302,13 +306,13 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
}
$body = $swooleRequest->getContent() ?? '';
$body = $swooleRequest->getContent() ?: '';
$method = $swooleRequest->server['request_method'];
$requestHeaders = $request->getHeaders();
if ($resource->isEmpty() || !$resource->getAttribute('enabled')) {
if ($type === 'functions') {
if ($type === 'function') {
throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND, view: $errorView);
} else {
throw new AppwriteException(AppwriteException::SITE_NOT_FOUND, view: $errorView);
@@ -330,7 +334,6 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$runtime = match ($type) {
'function' => $runtimes[$resource->getAttribute('runtime')] ?? null,
'site' => $runtimes[$resource->getAttribute('buildRuntime')] ?? null,
default => null
};
// Static site enforced runtime
@@ -459,10 +462,10 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
// V2 vars
if ($version === 'v2') {
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'],
'APPWRITE_FUNCTION_DATA' => $body,
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'],
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt']
]);
}
@@ -678,9 +681,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if (\is_string($logs) && \strlen($logs) > $maxLogLength) {
$warningMessage = "[WARNING] Logs truncated. The output exceeded {$maxLogLength} characters.\n";
$warningLength = \strlen($warningMessage);
$maxContentLength = max(0, $maxLogLength - $warningLength);
$logs = $warningMessage . ($maxContentLength > 0 ? \substr($logs, -$maxContentLength) : '');
$maxContentLength = $maxLogLength - \strlen($warningMessage);
$logs = $warningMessage . \substr($logs, -$maxContentLength);
}
// Truncate errors if they exceed the limit
@@ -689,9 +691,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if (\is_string($errors) && \strlen($errors) > $maxErrorLength) {
$warningMessage = "[WARNING] Errors truncated. The output exceeded {$maxErrorLength} characters.\n";
$warningLength = \strlen($warningMessage);
$maxContentLength = max(0, $maxErrorLength - $warningLength);
$errors = $warningMessage . ($maxContentLength > 0 ? \substr($errors, -$maxContentLength) : '');
$maxContentLength = $maxErrorLength - \strlen($warningMessage);
$errors = $warningMessage . \substr($errors, -$maxContentLength);
}
/** Update execution status */
$status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed';
@@ -719,14 +720,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
throw $th;
}
} finally {
if ($type === 'function' || $type === 'site') {
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
spec: $spec,
resource: $resource->getArrayCopy(),
));
}
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
spec: $spec,
resource: $resource->getArrayCopy(),
));
}
$execution->setAttribute('logs', '');
@@ -852,7 +851,7 @@ Http::init()
/*
* Appwrite Router
*/
$hostname = $request->getHostname() ?? '';
$hostname = $request->getHostname();
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
@@ -897,6 +896,9 @@ Http::init()
if (version_compare($requestFormat, '1.9.1', '<')) {
$request->addFilter(new RequestV22());
}
if (version_compare($requestFormat, '1.9.2', '<')) {
$request->addFilter(new RequestV23());
}
}
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
@@ -921,6 +923,9 @@ Http::init()
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
if (version_compare($responseFormat, '1.9.2', '<')) {
$response->addFilter(new ResponseV23());
}
if (version_compare($responseFormat, '1.9.1', '<')) {
$response->addFilter(new ResponseV22());
}
@@ -1014,11 +1019,11 @@ Http::init()
->inject('request')
->inject('console')
->inject('dbForPlatform')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('platform')
->inject('authorization')
->inject('certifiedDomains')
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) {
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $publisherForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) {
$hostname = $request->getHostname();
$platformHostnames = $platform['hostnames'] ?? [];
@@ -1044,7 +1049,7 @@ Http::init()
}
// 4. Check/create rule (requires DB access)
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, $certifiedDomains) {
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $publisherForCertificates, $certifiedDomains) {
try {
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
@@ -1100,10 +1105,11 @@ Http::init()
$dbForPlatform->createDocument('rules', $document);
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
$queueForCertificates
->setDomain($document)
->setSkipRenewCheck(true)
->trigger();
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: $console,
domain: $document,
skipRenewCheck: true,
));
} catch (Duplicate $e) {
Console::info('Certificate already exists');
} finally {
@@ -1498,9 +1504,9 @@ Http::error()
->setParam('development', Http::isDevelopment())
->setParam('projectName', $project->getAttribute('name'))
->setParam('projectURL', $project->getAttribute('url'))
->setParam('message', $output['message'] ?? '')
->setParam('type', $output['type'] ?? '')
->setParam('code', $output['code'] ?? '')
->setParam('message', $output['message'])
->setParam('type', $output['type'])
->setParam('code', $output['code'])
->setParam('trace', $output['trace'] ?? [])
->setParam('exception', $error);
@@ -1615,7 +1621,7 @@ Http::get('/.well-known/acme-challenge/*')
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND, 'Unknown path');
}
if (!\substr($absolute, 0, \strlen($base)) === $base) {
if (\substr($absolute, 0, \strlen($base)) !== $base) {
throw new AppwriteException(AppwriteException::GENERAL_UNAUTHORIZED_SCOPE, 'Invalid path');
}
@@ -1694,7 +1700,7 @@ Http::get('/_appwrite/authorize')
->inject('previewHostname')
->action(function (Request $request, Response $response, string $previewHostname) {
$host = $request->getHostname() ?? '';
$host = $request->getHostname();
if (!empty($previewHostname)) {
$host = $previewHostname;
}
+1 -1
View File
@@ -251,7 +251,7 @@ Http::get('/v1/mock/github/callback')
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId) ?? '';
$owner = $github->getOwnerName($providerInstallationId);
$projectInternalId = $project->getSequence();
+46 -44
View File
@@ -3,15 +3,17 @@
use Appwrite\Auth\Key;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Message\Audit as AuditMessage;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Messaging;
use Appwrite\Event\Publisher\Audit;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
@@ -42,9 +44,9 @@ use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Validator\WhiteList;
$parseLabel = function (string $label, array $responsePayload, array $requestParams, User $user) {
$parseLabel = function (string $label, array $responsePayload, array $requestParams, User $user, Document $project) {
preg_match_all('/{(.*?)}/', $label, $matches);
foreach ($matches[1] ?? [] as $pos => $match) {
foreach ($matches[1] as $pos => $match) {
$find = $matches[0][$pos];
$parts = explode('.', $match);
@@ -52,11 +54,12 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
throw new Exception(Exception::GENERAL_SERVER_ERROR, "The server encountered an error while parsing the label: $label. Please create an issue on GitHub to allow us to investigate further https://github.com/appwrite/appwrite/issues/new/choose");
}
$namespace = $parts[0] ?? '';
$replace = $parts[1] ?? '';
$namespace = $parts[0];
$replace = $parts[1];
$params = match ($namespace) {
'user' => (array) $user,
'project' => $project->getArrayCopy(),
'request' => $requestParams,
default => $responsePayload,
};
@@ -88,7 +91,7 @@ Http::init()
->inject('request')
->inject('dbForPlatform')
->inject('dbForProject')
->inject('queueForAudits')
->inject('auditContext')
->inject('project')
->inject('user')
->inject('session')
@@ -97,7 +100,7 @@ Http::init()
->inject('team')
->inject('apiKey')
->inject('authorization')
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
@@ -193,7 +196,7 @@ Http::init()
'name' => $apiKey->getName(),
]);
$queueForAudits->setUser($user);
$auditContext->user = $user;
}
// For standard keys, update last accessed time
@@ -261,10 +264,9 @@ Http::init()
$userClone->setAttribute('type', match ($apiKey->getType()) {
API_KEY_STANDARD => ACTIVITY_TYPE_KEY_PROJECT,
API_KEY_ACCOUNT => ACTIVITY_TYPE_KEY_ACCOUNT,
API_KEY_ORGANIZATION => ACTIVITY_TYPE_KEY_ORGANIZATION,
default => ACTIVITY_TYPE_KEY_PROJECT,
default => ACTIVITY_TYPE_KEY_ORGANIZATION,
});
$queueForAudits->setUser($userClone);
$auditContext->user = $userClone;
}
// Apply permission
@@ -383,7 +385,7 @@ Http::init()
}
// Step 6: Update project and user last activity
if (! $project->isEmpty() && $project->getId() !== 'console') {
if ($project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
@@ -413,9 +415,6 @@ Http::init()
}
// Steps 7-9: Access Control - Method, Namespace and Scope Validation
/**
* @var ?Method $method
*/
$method = $route->getLabel('sdk', false);
// Take the first method if there's more than one,
@@ -486,7 +485,7 @@ Http::init()
->inject('user')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('queueForAudits')
->inject('auditContext')
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('queueForBuilds')
@@ -503,7 +502,7 @@ Http::init()
->inject('telemetry')
->inject('platform')
->inject('authorization')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
$response->setUser($user);
$request->setUser($user);
@@ -596,13 +595,12 @@ Http::init()
->setProject($project)
->setUser($user);
$queueForAudits
->setMode($mode)
->setUserAgent($request->getUserAgent(''))
->setIP($request->getIP())
->setHostname($request->getHostname())
->setEvent($route->getLabel('audits.event', ''))
->setProject($project);
$auditContext->mode = $mode;
$auditContext->userAgent = $request->getUserAgent('');
$auditContext->ip = $request->getIP();
$auditContext->hostname = $request->getHostname();
$auditContext->event = $route->getLabel('audits.event', '');
$auditContext->project = $project;
/* If a session exists, use the user associated with the session */
if (! $user->isEmpty()) {
@@ -611,7 +609,7 @@ Http::init()
if (empty($user->getAttribute('type'))) {
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
}
$queueForAudits->setUser($userClone);
$auditContext->user = $userClone;
}
/* Auto-set projects */
@@ -645,7 +643,7 @@ Http::init()
if (! empty($data) && ! $cacheLog->isEmpty()) {
$parts = explode('/', $cacheLog->getAttribute('resourceType', ''));
$type = $parts[0] ?? null;
$type = $parts[0];
if ($type === 'bucket' && (! $isImageTransformation || ! $isDisabled)) {
$bucketId = $parts[1] ?? null;
@@ -756,7 +754,12 @@ Http::shutdown()
->inject('project')
->inject('dbForProject')
->action(function (Http $utopia, Request $request, Response $response, Document $project, Database $dbForProject) {
$sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT;
$sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? 0;
if ($sessionLimit === 0) {
return;
}
$session = $response->getPayload();
$userId = $session['userId'] ?? '';
if (empty($userId)) {
@@ -790,7 +793,8 @@ Http::shutdown()
->inject('project')
->inject('user')
->inject('queueForEvents')
->inject('queueForAudits')
->inject('auditContext')
->inject('publisherForAudits')
->inject('usage')
->inject('publisherForUsage')
->inject('queueForDeletes')
@@ -807,7 +811,7 @@ Http::shutdown()
->inject('bus')
->inject('apiKey')
->inject('mode')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -900,9 +904,9 @@ Http::shutdown()
*/
$pattern = $route->getLabel('audits.resource', null);
if (! empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user, $project);
if (! empty($resource) && $resource !== $pattern) {
$queueForAudits->setResource($resource);
$auditContext->resource = $resource;
}
}
@@ -912,8 +916,8 @@ Http::shutdown()
if (empty($user->getAttribute('type'))) {
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
}
$queueForAudits->setUser($userClone);
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
$auditContext->user = $userClone;
} elseif ($auditContext->user === null || $auditContext->user->isEmpty()) {
/**
* User in the request is empty, and no user was set for auditing previously.
* This indicates:
@@ -931,24 +935,21 @@ Http::shutdown()
'name' => 'Guest',
]);
$queueForAudits->setUser($user);
$auditContext->user = $user;
}
if (! empty($queueForAudits->getResource()) && ! $queueForAudits->getUser()->isEmpty()) {
$auditUser = $auditContext->user;
if (! empty($auditContext->resource) && ! $auditUser->isEmpty()) {
/**
* audits.payload is switched to default true
* in order to auto audit payload for all endpoints
*/
$pattern = $route->getLabel('audits.payload', true);
if (! empty($pattern)) {
$queueForAudits->setPayload($responsePayload);
$auditContext->payload = $responsePayload;
}
foreach ($queueForEvents->getParams() as $key => $value) {
$queueForAudits->setParam($key, $value);
}
$queueForAudits->trigger();
$publisherForAudits->enqueue(AuditMessage::fromContext($auditContext));
}
if (! empty($queueForDeletes->getType())) {
@@ -972,15 +973,16 @@ Http::shutdown()
if ($useCache) {
$resource = $resourceType = null;
$data = $response->getPayload();
if (! empty($data['payload'])) {
$statusCode = $response->getStatusCode();
if (! empty($data['payload']) && $statusCode >= 200 && $statusCode < 300) {
$pattern = $route->getLabel('cache.resource', null);
if (! empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user, $project);
}
$pattern = $route->getLabel('cache.resourceType', null);
if (! empty($pattern)) {
$resourceType = $parseLabel($pattern, $responsePayload, $requestParams, $user);
$resourceType = $parseLabel($pattern, $responsePayload, $requestParams, $user, $project);
}
$cache = new Cache(
+7 -16
View File
@@ -72,8 +72,6 @@ $swooleAdapter = new Server(
container: $container,
);
$container->set('container', fn () => fn () => $swooleAdapter->getContainer());
$http = $swooleAdapter->getServer();
/**
@@ -415,27 +413,19 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
$projectCollections = $collections['projects'];
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
$sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
$sharedTablesV2 = \array_diff($sharedTables, $sharedTablesV1);
$documentsSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
$documentsSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', ''));
$documentsSharedTablesV2 = \array_diff($documentsSharedTables, $documentsSharedTablesV1);
$vectorSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
$vectorSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', ''));
$vectorSharedTablesV2 = \array_diff($vectorSharedTables, $vectorSharedTablesV1);
$cache = $app->getResource('cache');
// All shared tables V2 pools that need project metadata collections
$sharedTablesV2All = \array_values(\array_unique(\array_filter([
...$sharedTablesV2,
...$documentsSharedTablesV2,
...$vectorSharedTablesV2,
// All shared tables pools that need project metadata collections
$allSharedTables = \array_values(\array_unique(\array_filter([
...$sharedTables,
...$documentsSharedTables,
...$vectorSharedTables,
])));
foreach ($sharedTablesV2All as $hostname) {
foreach ($allSharedTables as $hostname) {
Span::init('database.setup');
Span::add('database.hostname', $hostname);
@@ -533,6 +523,7 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
}
$requestContainer = $swooleAdapter->getContainer();
$requestContainer->set('container', fn () => $requestContainer);
$requestContainer->set('request', fn () => $request);
$requestContainer->set('response', fn () => $response);
+4 -6
View File
@@ -1,6 +1,7 @@
<?php
use Appwrite\Platform\Modules\Compute\Specification;
use Utopia\System\System;
const APP_NAME = 'Appwrite';
const APP_DOMAIN = 'appwrite.io';
@@ -24,9 +25,6 @@ const APP_MODE_ADMIN = 'admin';
const APP_PAGING_LIMIT = 12;
const APP_LIMIT_COUNT = 5000;
const APP_LIMIT_USERS = 10_000;
const APP_LIMIT_USER_PASSWORD_HISTORY = 20;
const APP_LIMIT_USER_SESSIONS_MAX = 100;
const APP_LIMIT_USER_SESSIONS_DEFAULT = 10;
const APP_LIMIT_ANTIVIRUS = 20_000_000; //20MB
const APP_LIMIT_ENCRYPTION = 20_000_000; //20MB
const APP_LIMIT_COMPRESSION = 20_000_000; //20MB
@@ -46,8 +44,8 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 4322;
const APP_VERSION_STABLE = '1.9.1';
const APP_CACHE_BUSTER = 4323;
const APP_VERSION_STABLE = '1.9.2';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
@@ -190,7 +188,7 @@ const BUILD_TYPE_RETRY = 'retry';
// Deletion Types
const ENABLE_EXECUTIONS_LIMIT_ON_ROUTE = false;
\define('ENABLE_EXECUTIONS_LIMIT_ON_ROUTE', System::getEnv('_APP_EXECUTIONS_LIMIT_ON_ROUTE', 'disabled') === 'enabled');
const DELETE_TYPE_DATABASES = 'databases';
const DELETE_TYPE_DOCUMENT = 'document';
+114 -4
View File
@@ -56,6 +56,9 @@ use Appwrite\Utopia\Response\Model\ColumnString;
use Appwrite\Utopia\Response\Model\ColumnText;
use Appwrite\Utopia\Response\Model\ColumnURL;
use Appwrite\Utopia\Response\Model\ColumnVarchar;
use Appwrite\Utopia\Response\Model\ConsoleOAuth2Provider;
use Appwrite\Utopia\Response\Model\ConsoleOAuth2ProviderList;
use Appwrite\Utopia\Response\Model\ConsoleOAuth2ProviderParameter;
use Appwrite\Utopia\Response\Model\ConsoleVariables;
use Appwrite\Utopia\Response\Model\Continent;
use Appwrite\Utopia\Response\Model\Country;
@@ -105,6 +108,47 @@ use Appwrite\Utopia\Response\Model\MigrationReport;
use Appwrite\Utopia\Response\Model\Mock;
use Appwrite\Utopia\Response\Model\MockNumber;
use Appwrite\Utopia\Response\Model\None;
use Appwrite\Utopia\Response\Model\OAuth2Amazon;
use Appwrite\Utopia\Response\Model\OAuth2Apple;
use Appwrite\Utopia\Response\Model\OAuth2Auth0;
use Appwrite\Utopia\Response\Model\OAuth2Authentik;
use Appwrite\Utopia\Response\Model\OAuth2Autodesk;
use Appwrite\Utopia\Response\Model\OAuth2Bitbucket;
use Appwrite\Utopia\Response\Model\OAuth2Bitly;
use Appwrite\Utopia\Response\Model\OAuth2Box;
use Appwrite\Utopia\Response\Model\OAuth2Dailymotion;
use Appwrite\Utopia\Response\Model\OAuth2Discord;
use Appwrite\Utopia\Response\Model\OAuth2Disqus;
use Appwrite\Utopia\Response\Model\OAuth2Dropbox;
use Appwrite\Utopia\Response\Model\OAuth2Etsy;
use Appwrite\Utopia\Response\Model\OAuth2Facebook;
use Appwrite\Utopia\Response\Model\OAuth2Figma;
use Appwrite\Utopia\Response\Model\OAuth2FusionAuth;
use Appwrite\Utopia\Response\Model\OAuth2GitHub;
use Appwrite\Utopia\Response\Model\OAuth2Gitlab;
use Appwrite\Utopia\Response\Model\OAuth2Google;
use Appwrite\Utopia\Response\Model\OAuth2Keycloak;
use Appwrite\Utopia\Response\Model\OAuth2Kick;
use Appwrite\Utopia\Response\Model\OAuth2Linkedin;
use Appwrite\Utopia\Response\Model\OAuth2Microsoft;
use Appwrite\Utopia\Response\Model\OAuth2Notion;
use Appwrite\Utopia\Response\Model\OAuth2Oidc;
use Appwrite\Utopia\Response\Model\OAuth2Okta;
use Appwrite\Utopia\Response\Model\OAuth2Paypal;
use Appwrite\Utopia\Response\Model\OAuth2Podio;
use Appwrite\Utopia\Response\Model\OAuth2ProviderList;
use Appwrite\Utopia\Response\Model\OAuth2Salesforce;
use Appwrite\Utopia\Response\Model\OAuth2Slack;
use Appwrite\Utopia\Response\Model\OAuth2Spotify;
use Appwrite\Utopia\Response\Model\OAuth2Stripe;
use Appwrite\Utopia\Response\Model\OAuth2Tradeshift;
use Appwrite\Utopia\Response\Model\OAuth2Twitch;
use Appwrite\Utopia\Response\Model\OAuth2WordPress;
use Appwrite\Utopia\Response\Model\OAuth2X;
use Appwrite\Utopia\Response\Model\OAuth2Yahoo;
use Appwrite\Utopia\Response\Model\OAuth2Yandex;
use Appwrite\Utopia\Response\Model\OAuth2Zoho;
use Appwrite\Utopia\Response\Model\OAuth2Zoom;
use Appwrite\Utopia\Response\Model\Phone;
use Appwrite\Utopia\Response\Model\PlatformAndroid;
use Appwrite\Utopia\Response\Model\PlatformApple;
@@ -112,12 +156,24 @@ use Appwrite\Utopia\Response\Model\PlatformLinux;
use Appwrite\Utopia\Response\Model\PlatformList;
use Appwrite\Utopia\Response\Model\PlatformWeb;
use Appwrite\Utopia\Response\Model\PlatformWindows;
use Appwrite\Utopia\Response\Model\PolicyList;
use Appwrite\Utopia\Response\Model\PolicyMembershipPrivacy;
use Appwrite\Utopia\Response\Model\PolicyPasswordDictionary;
use Appwrite\Utopia\Response\Model\PolicyPasswordHistory;
use Appwrite\Utopia\Response\Model\PolicyPasswordPersonalData;
use Appwrite\Utopia\Response\Model\PolicySessionAlert;
use Appwrite\Utopia\Response\Model\PolicySessionDuration;
use Appwrite\Utopia\Response\Model\PolicySessionInvalidation;
use Appwrite\Utopia\Response\Model\PolicySessionLimit;
use Appwrite\Utopia\Response\Model\PolicyUserLimit;
use Appwrite\Utopia\Response\Model\Preferences;
use Appwrite\Utopia\Response\Model\Project;
use Appwrite\Utopia\Response\Model\Provider;
use Appwrite\Utopia\Response\Model\ProviderRepository;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFrameworkList;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntimeList;
use Appwrite\Utopia\Response\Model\ResourceToken;
use Appwrite\Utopia\Response\Model\Row;
use Appwrite\Utopia\Response\Model\Rule;
@@ -135,7 +191,6 @@ use Appwrite\Utopia\Response\Model\TemplateFramework;
use Appwrite\Utopia\Response\Model\TemplateFunction;
use Appwrite\Utopia\Response\Model\TemplateRuntime;
use Appwrite\Utopia\Response\Model\TemplateSite;
use Appwrite\Utopia\Response\Model\TemplateSMS;
use Appwrite\Utopia\Response\Model\TemplateVariable;
use Appwrite\Utopia\Response\Model\Token;
use Appwrite\Utopia\Response\Model\Topic;
@@ -190,8 +245,8 @@ Response::setModel(new BaseList('Site Templates List', Response::MODEL_TEMPLATE_
Response::setModel(new BaseList('Functions List', Response::MODEL_FUNCTION_LIST, 'functions', Response::MODEL_FUNCTION));
Response::setModel(new BaseList('Function Templates List', Response::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', Response::MODEL_TEMPLATE_FUNCTION));
Response::setModel(new BaseList('Installations List', Response::MODEL_INSTALLATION_LIST, 'installations', Response::MODEL_INSTALLATION));
Response::setModel(new BaseList('Framework Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, 'frameworkProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK));
Response::setModel(new BaseList('Runtime Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, 'runtimeProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME));
Response::setModel(new ProviderRepositoryFrameworkList());
Response::setModel(new ProviderRepositoryRuntimeList());
Response::setModel(new BaseList('Branches List', Response::MODEL_BRANCH_LIST, 'branches', Response::MODEL_BRANCH));
Response::setModel(new BaseList('Frameworks List', Response::MODEL_FRAMEWORK_LIST, 'frameworks', Response::MODEL_FRAMEWORK));
Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, 'runtimes', Response::MODEL_RUNTIME));
@@ -209,6 +264,9 @@ Response::setModel(new BaseList('Currencies List', Response::MODEL_CURRENCY_LIST
Response::setModel(new BaseList('Phones List', Response::MODEL_PHONE_LIST, 'phones', Response::MODEL_PHONE));
Response::setModel(new BaseList('Metric List', Response::MODEL_METRIC_LIST, 'metrics', Response::MODEL_METRIC, true, false));
Response::setModel(new BaseList('Variables List', Response::MODEL_VARIABLE_LIST, 'variables', Response::MODEL_VARIABLE));
Response::setModel(new BaseList('Mock Numbers List', Response::MODEL_MOCK_NUMBER_LIST, 'mockNumbers', Response::MODEL_MOCK_NUMBER));
Response::setModel(new PolicyList());
Response::setModel(new BaseList('Email Templates List', Response::MODEL_EMAIL_TEMPLATE_LIST, 'templates', Response::MODEL_EMAIL_TEMPLATE));
Response::setModel(new BaseList('Status List', Response::MODEL_HEALTH_STATUS_LIST, 'statuses', Response::MODEL_HEALTH_STATUS));
Response::setModel(new BaseList('Rule List', Response::MODEL_PROXY_RULE_LIST, 'rules', Response::MODEL_PROXY_RULE));
Response::setModel(new BaseList('Schedules List', Response::MODEL_SCHEDULE_LIST, 'schedules', Response::MODEL_SCHEDULE));
@@ -336,6 +394,56 @@ Response::setModel(new Webhook());
Response::setModel(new Key());
Response::setModel(new DevKey());
Response::setModel(new MockNumber());
Response::setModel(new OAuth2GitHub());
Response::setModel(new OAuth2Discord());
Response::setModel(new OAuth2Figma());
Response::setModel(new OAuth2Dropbox());
Response::setModel(new OAuth2Dailymotion());
Response::setModel(new OAuth2Bitbucket());
Response::setModel(new OAuth2Bitly());
Response::setModel(new OAuth2Box());
Response::setModel(new OAuth2Autodesk());
Response::setModel(new OAuth2Google());
Response::setModel(new OAuth2Zoom());
Response::setModel(new OAuth2Zoho());
Response::setModel(new OAuth2Yandex());
Response::setModel(new OAuth2X());
Response::setModel(new OAuth2WordPress());
Response::setModel(new OAuth2Twitch());
Response::setModel(new OAuth2Stripe());
Response::setModel(new OAuth2Spotify());
Response::setModel(new OAuth2Slack());
Response::setModel(new OAuth2Podio());
Response::setModel(new OAuth2Notion());
Response::setModel(new OAuth2Salesforce());
Response::setModel(new OAuth2Yahoo());
Response::setModel(new OAuth2Linkedin());
Response::setModel(new OAuth2Disqus());
Response::setModel(new OAuth2Amazon());
Response::setModel(new OAuth2Etsy());
Response::setModel(new OAuth2Facebook());
Response::setModel(new OAuth2Tradeshift());
Response::setModel(new OAuth2Paypal());
Response::setModel(new OAuth2Gitlab());
Response::setModel(new OAuth2Authentik());
Response::setModel(new OAuth2Auth0());
Response::setModel(new OAuth2FusionAuth());
Response::setModel(new OAuth2Keycloak());
Response::setModel(new OAuth2Oidc());
Response::setModel(new OAuth2Okta());
Response::setModel(new OAuth2Kick());
Response::setModel(new OAuth2Apple());
Response::setModel(new OAuth2Microsoft());
Response::setModel(new OAuth2ProviderList());
Response::setModel(new PolicyPasswordDictionary());
Response::setModel(new PolicyPasswordHistory());
Response::setModel(new PolicyPasswordPersonalData());
Response::setModel(new PolicySessionAlert());
Response::setModel(new PolicySessionDuration());
Response::setModel(new PolicySessionInvalidation());
Response::setModel(new PolicySessionLimit());
Response::setModel(new PolicyUserLimit());
Response::setModel(new PolicyMembershipPrivacy());
Response::setModel(new AuthProvider());
Response::setModel(new PlatformWeb());
Response::setModel(new PlatformApple());
@@ -373,9 +481,11 @@ Response::setModel(new Headers());
Response::setModel(new Specification());
Response::setModel(new Rule());
Response::setModel(new Schedule());
Response::setModel(new TemplateSMS());
Response::setModel(new TemplateEmail());
Response::setModel(new ConsoleVariables());
Response::setModel(new ConsoleOAuth2ProviderParameter());
Response::setModel(new ConsoleOAuth2Provider());
Response::setModel(new ConsoleOAuth2ProviderList());
Response::setModel(new MFAChallenge());
Response::setModel(new MFARecoveryCodes());
Response::setModel(new MFAType());
+7 -5
View File
@@ -85,7 +85,7 @@ return function (Container $container): void {
return $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]) ?? new Document();
]);
});
$permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence();
@@ -139,7 +139,7 @@ return function (Container $container): void {
$sdkValidator = new WhiteList($servers, true);
$sdk = \strtolower($request->getHeader('x-sdk-name', 'UNKNOWN'));
if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) {
if ($sdk !== 'unknown' && $sdkValidator->isValid($sdk)) {
$sdks = $key->getAttribute('sdks', []);
if (!\in_array($sdk, $sdks, true)) {
@@ -327,9 +327,11 @@ return function (Container $container): void {
}
}
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', '');
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', '');
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', '');
// Query params mirror the header fallback pattern used by ?project= and ?devKey=,
// allowing Console to embed impersonation in direct file/image URLs where headers cannot be set.
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', (string)$request->getParam('impersonateUserId', ''));
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', (string)$request->getParam('impersonateEmail', ''));
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', (string)$request->getParam('impersonatePhone', ''));
if (!$user->isEmpty() && $user->getAttribute('impersonator', false)) {
$userDb = ($mode === APP_MODE_ADMIN || $project->getId() === 'console') ? $dbForPlatform : $dbForProject;
+6 -7
View File
@@ -71,7 +71,7 @@ $register->set('logger', function () {
$providerConfig = match ($providerName) {
'sentry' => [ 'key' => $configChunks[0], 'projectId' => $configChunks[1] ?? '', 'host' => '',],
'logowl' => ['ticket' => $configChunks[0] ?? '', 'host' => ''],
'logowl' => ['ticket' => $configChunks[0], 'host' => ''],
default => ['key' => $providerConfig],
};
}
@@ -249,11 +249,11 @@ $register->set('pools', function () {
$poolSize = max(1, (int)($instanceConnections / $workerCount));
foreach ($connections as $key => $connection) {
$type = $connection['type'] ?? '';
$multiple = $connection['multiple'] ?? false;
$schemes = $connection['schemes'] ?? [];
$type = $connection['type'];
$multiple = $connection['multiple'];
$schemes = $connection['schemes'];
$config = [];
$dsns = explode(',', $connection['dsns'] ?? '');
$dsns = explode(',', $connection['dsns']);
foreach ($dsns as &$dsn) {
$dsn = explode('=', $dsn);
$name = ($multiple) ? $key . '_' . $dsn[0] : $key;
@@ -318,7 +318,7 @@ $register->set('pools', function () {
));
});
},
'redis' => function () use ($dsnHost, $dsnPort, $dsnPass) {
default => function () use ($dsnHost, $dsnPort, $dsnPass) {
$redis = new \Redis();
@$redis->pconnect($dsnHost, (int)$dsnPort);
if ($dsnPass) {
@@ -328,7 +328,6 @@ $register->set('pools', function () {
return $redis;
},
default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Invalid scheme'),
};
$poolAdapter = System::getEnv('_APP_POOL_ADAPTER', default: 'stack') === 'swoole' ? new SwoolePool() : new StackPool();
+16 -1
View File
@@ -1,8 +1,11 @@
<?php
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Audit as AuditPublisher;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
use Appwrite\Event\Publisher\Screenshot as ScreenshotPublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Utopia\Database\Documents\User;
@@ -81,6 +84,18 @@ $container->set('publisherMessaging', function (Publisher $publisher) {
$container->set('publisherWebhooks', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherForAudits', fn (Publisher $publisher) => new AuditPublisher(
$publisher,
new Queue(System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
$publisher,
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForScreenshots', fn (Publisher $publisher) => new ScreenshotPublisher(
$publisher,
new Queue(System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
@@ -251,7 +266,7 @@ function getDevice(string $root, string $connection = ''): Device
return new Local($root);
}
} else {
switch (strtolower(System::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) ?? '')) {
switch (strtolower(System::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL))) {
case Storage::DEVICE_LOCAL:
default:
return new Local($root);
+12 -24
View File
@@ -4,9 +4,8 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Audit as AuditEvent;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
@@ -14,7 +13,6 @@ use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
@@ -128,9 +126,6 @@ return function (Container $container): void {
$container->set('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
$container->set('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
$container->set('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
@@ -149,18 +144,13 @@ return function (Container $container): void {
$container->set('usage', function () {
return new UsageContext();
}, []);
$container->set('queueForAudits', function (Publisher $publisher) {
return new AuditEvent($publisher);
}, ['publisher']);
$container->set('auditContext', fn () => new AuditContext(), []);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('eventProcessor', function () {
return new EventProcessor();
}, []);
$container->set('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
$container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
$adapter = new DatabasePool($pools->get('console'));
$database = new Database($adapter, $cache);
@@ -385,7 +375,7 @@ return function (Container $container): void {
return $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]) ?? new Document();
]);
});
$permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence();
@@ -488,14 +478,10 @@ return function (Container $container): void {
}
// Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies
if ($response) { // if in http context - add debug header
$response->addHeader('X-Debug-Fallback', 'false');
}
$response->addHeader('X-Debug-Fallback', 'false');
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
if ($response) {
$response->addHeader('X-Debug-Fallback', 'true');
}
$response->addHeader('X-Debug-Fallback', 'true');
$fallback = $request->getHeader('x-fallback-cookies', '');
$fallback = \json_decode($fallback, true);
$store->decode(((is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : ''));
@@ -585,10 +571,12 @@ return function (Container $container): void {
}
}
// Impersonation: if current user has impersonator capability and headers are set, act as another user
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', '');
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', '');
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', '');
// Impersonation: if current user has impersonator capability and headers/params are set, act as another user
// Query params mirror the header fallback pattern used by ?project= and ?devKey=,
// allowing Console to embed impersonation in direct file/image URLs where headers cannot be set.
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', (string)$request->getParam('impersonateUserId', ''));
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', (string)$request->getParam('impersonateEmail', ''));
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', (string)$request->getParam('impersonatePhone', ''));
if (!$user->isEmpty() && $user->getAttribute('impersonator', false)) {
$userDb = (APP_MODE_ADMIN === $mode || $project->getId() === 'console') ? $dbForPlatform : $dbForProject;
$targetUser = null;
@@ -1094,7 +1082,7 @@ return function (Container $container): void {
$sdkValidator = new WhiteList($servers, true);
$sdk = \strtolower($request->getHeader('x-sdk-name', 'UNKNOWN'));
if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) {
if ($sdk !== 'unknown' && $sdkValidator->isValid($sdk)) {
$sdks = $key->getAttribute('sdks', []);
if (! in_array($sdk, $sdks)) {
+1 -16
View File
@@ -1,8 +1,6 @@
<?php
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
@@ -10,7 +8,6 @@ use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
@@ -311,10 +308,6 @@ return function (Container $container): void {
return new Build($publisher);
}, ['publisher']);
$container->set('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
@@ -323,10 +316,6 @@ return function (Container $container): void {
return new Event($publisher);
}, ['publisher']);
$container->set('queueForAudits', function (Publisher $publisher) {
return new Audit($publisher);
}, ['publisher']);
$container->set('queueForWebhooks', function (Publisher $publisher) {
return new Webhook($publisher);
}, ['publisher']);
@@ -339,10 +328,6 @@ return function (Container $container): void {
return new Realtime();
}, []);
$container->set('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
$container->set('deviceForSites', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
@@ -383,7 +368,7 @@ return function (Container $container): void {
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', \get_class($error));
$log->addTag('projectId', $project->getId() ?? '');
$log->addTag('projectId', $project->getId());
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
+294 -18
View File
@@ -38,12 +38,14 @@ use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Registry\Registry;
use Utopia\Span\Span;
use Utopia\System\System;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
use Utopia\WebSocket\Adapter;
use Utopia\WebSocket\Server;
require_once __DIR__ . '/init.php';
require_once __DIR__ . '/init/span.php';
/** @var Registry $register */
$register = $GLOBALS['register'] ?? throw new \RuntimeException('Registry not initialized');
@@ -262,7 +264,9 @@ $stats->create();
$containerId = uniqid();
$statsDocument = null;
$workerNumber = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$workerNumber = intval(System::getEnv('_APP_WORKERS_NUM', 0))
?: intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$adapter = new Adapter\Swoole(port: System::getEnv('PORT', 80));
$adapter
@@ -394,10 +398,27 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
Console::success('Worker ' . $workerId . ' started successfully');
$telemetry = getTelemetry($workerId);
$realtimeDelayBuckets = [100, 250, 500, 750, 1000, 1500, 2000, 3000, 5000, 7500, 10000, 15000, 30000];
$workerTelemetryAttributes = ['workerId' => (string) $workerId];
$register->set('telemetry', fn () => $telemetry);
$register->set('telemetry.workerAttributes', fn () => $workerTelemetryAttributes);
$register->set('telemetry.workerCounter', fn () => $telemetry->createUpDownCounter('realtime.server.active_workers'));
$register->set('telemetry.workerClientCounter', fn () => $telemetry->createUpDownCounter('realtime.server.worker_clients'));
$register->set('telemetry.workerSubscriptionCounter', fn () => $telemetry->createUpDownCounter('realtime.server.worker_subscriptions'));
$register->set('telemetry.connectionCounter', fn () => $telemetry->createUpDownCounter('realtime.server.open_connections'));
$register->set('telemetry.connectionCreatedCounter', fn () => $telemetry->createCounter('realtime.server.connection.created'));
$register->set('telemetry.messageSentCounter', fn () => $telemetry->createCounter('realtime.server.message.sent'));
$register->set('telemetry.deliveryDelayHistogram', fn () => $telemetry->createHistogram(
name: 'realtime.server.delivery_delay',
unit: 'ms',
advisory: ['ExplicitBucketBoundaries' => $realtimeDelayBuckets],
));
$register->set('telemetry.arrivalDelayHistogram', fn () => $telemetry->createHistogram(
name: 'realtime.server.arrival_delay',
unit: 'ms',
advisory: ['ExplicitBucketBoundaries' => $realtimeDelayBuckets],
));
$register->get('telemetry.workerCounter')->add(1);
$attempts = 0;
$start = time();
@@ -514,12 +535,28 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$pubsub->subscribe(['realtime'], function (mixed $redis, string $channel, string $payload) use ($server, $workerId, $stats, $register, $realtime) {
$event = json_decode($payload, true);
$eventTimestamp = $event['data']['timestamp'] ?? null;
if (\is_string($eventTimestamp)) {
try {
$eventDate = new \DateTimeImmutable($eventTimestamp, new \DateTimeZone('UTC'));
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
$eventTimestampMs = (float) $eventDate->format('U.u') * 1000;
$nowTimestampMs = (float) $now->format('U.u') * 1000;
$arrivalDelayMs = (int) \max(0, $nowTimestampMs - $eventTimestampMs);
$register->get('telemetry.arrivalDelayHistogram')->record($arrivalDelayMs);
} catch (\Throwable) {
// Ignore invalid timestamp payloads.
}
}
if ($event['permissionsChanged'] && isset($event['userId'])) {
$projectId = $event['project'];
$userId = $event['userId'];
if ($realtime->hasSubscriber($projectId, 'user:' . $userId)) {
$connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId]));
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
$consoleDatabase = getConsoleDB();
$project = $consoleDatabase->getAuthorization()->skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
$database = getProjectDB($project);
@@ -529,6 +566,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$roles = $user->getRoles($database->getAuthorization());
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
$previousUserId = $realtime->connections[$connection]['userId'] ?? '';
$meta = $realtime->getSubscriptionMetadata($connection);
@@ -536,13 +574,19 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
foreach ($meta as $subscriptionId => $subscription) {
$queries = Query::parseQueries($subscription['queries'] ?? []);
$channels = Realtime::rebindAccountChannels(
$subscription['channels'] ?? [],
$previousUserId,
$userId
);
$realtime->subscribe(
$projectId,
$connection,
$subscriptionId,
$roles,
$subscription['channels'] ?? [],
$queries
$channels,
$queries,
$userId
);
}
@@ -550,6 +594,12 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
if ($authorization !== null) {
$realtime->connections[$connection]['authorization'] = $authorization;
}
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
if ($subscriptionDelta !== 0) {
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
}
}
}
@@ -592,6 +642,20 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
if ($total > 0) {
$register->get('telemetry.messageSentCounter')->add($total);
$stats->incr($event['project'], 'messages', $total);
$updatedAt = $event['data']['payload']['$updatedAt'] ?? null;
if (\is_string($updatedAt)) {
try {
$updatedAtDate = new \DateTimeImmutable($updatedAt, new \DateTimeZone('UTC'));
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
$updatedAtTimestampMs = (float) $updatedAtDate->format('U.u') * 1000;
$nowTimestampMs = (float) $now->format('U.u') * 1000;
$delayMs = (int) \max(0, $nowTimestampMs - $updatedAtTimestampMs);
$register->get('telemetry.deliveryDelayHistogram')->record($delayMs);
} catch (\Throwable) {
// Ignore invalid timestamp payloads.
}
}
$projectId = $event['project'] ?? null;
@@ -621,6 +685,16 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
Console::error('Failed to restart pub/sub...');
});
$server->onWorkerStop(function (int $workerId) use ($register) {
Console::warning('Worker ' . $workerId . ' stopping');
try {
$register->get('telemetry.workerCounter')->add(-1);
} catch (\Throwable $th) {
Console::error('Realtime onWorkerStop telemetry error: ' . $th->getMessage());
}
});
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $registerConnectionResources) {
global $container;
$request = new Request($request);
@@ -636,6 +710,20 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$project = null;
$logUser = null;
$authorization = null;
$rawSize = $request->getSize();
$channelCount = 0;
$subscriptionCount = 0;
$outboundBytes = 0;
$responseCode = 200;
$subscriptionMode = 'message';
$success = false;
Span::init('realtime.open');
Span::add('realtime.connectionId', $connection);
Span::add('realtime.inboundBytes', $rawSize);
if (!empty($request->getOrigin())) {
Span::add('realtime.origin', $request->getOrigin());
}
try {
/** @var Document $project */
@@ -685,8 +773,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new Exception(Exception::REALTIME_TOO_MANY_MESSAGES, 'Too many requests');
}
$rawSize = $request->getSize();
triggerStats([
METRIC_REALTIME_INBOUND => $rawSize,
], $project->getId());
@@ -706,9 +792,11 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$roles = $user->getRoles($authorization);
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
$channelCount = \count($channels);
$updateStats = static function (string $projectId, ?string $teamId, string $payloadJson) use ($register, $stats): void {
$register->get('telemetry.connectionCounter')->add(1);
$register->get('telemetry.workerClientCounter')->add(1, $register->get('telemetry.workerAttributes'));
$register->get('telemetry.connectionCreatedCounter')->add(1);
$stats->set($projectId, [
@@ -742,11 +830,15 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$realtime->subscribe($project->getId(), $connection, '', $roles, [], [], $user->getId());
$realtime->connections[$connection]['authorization'] = $authorization;
$server->send([$connection], $connectedPayloadJson);
$outboundBytes += \strlen($connectedPayloadJson);
$updateStats($project->getId(), $project->getAttribute('teamId'), $connectedPayloadJson);
$subscriptionMode = 'message';
$success = true;
return;
}
$names = array_keys($channels);
$subscriptionMode = 'url';
try {
$subscriptions = Realtime::constructSubscriptions(
@@ -773,6 +865,10 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$mapping[$index] = $subscriptionId;
}
$subscriptionCount = \count($subscriptions);
if (!empty($subscriptions)) {
$register->get('telemetry.workerSubscriptionCounter')->add(\count($subscriptions), $register->get('telemetry.workerAttributes'));
}
$realtime->connections[$connection]['authorization'] = $authorization;
@@ -788,8 +884,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
]);
$server->send([$connection], $connectedPayloadJson);
$outboundBytes += \strlen($connectedPayloadJson);
$updateStats($project->getId(), $project->getAttribute('teamId'), $connectedPayloadJson);
$success = true;
} catch (Throwable $th) {
logError($th, 'realtime', project: $project, user: $logUser, authorization: $authorization);
@@ -799,6 +896,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
if (!\is_int($code)) {
$code = 500;
}
$responseCode = $code;
$message = $th->getMessage();
@@ -816,7 +914,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
]
];
$server->send([$connection], json_encode($response));
$responsePayloadJson = json_encode($response);
$server->send([$connection], $responsePayloadJson);
$outboundBytes += \strlen($responsePayloadJson);
$server->close($connection, $code);
if (System::getEnv('_APP_ENV', 'production') === 'development') {
@@ -824,16 +924,44 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
Console::error('[Error] Code: ' . $response['data']['code']);
Console::error('[Error] Message: ' . $response['data']['message']);
}
Span::error($th);
} finally {
Span::add('realtime.success', $success);
Span::add('realtime.responseCode', $responseCode);
Span::add('realtime.subscriptionMode', $subscriptionMode);
Span::add('realtime.channelCount', $channelCount);
Span::add('realtime.subscriptionCount', $subscriptionCount);
Span::add('realtime.outboundBytes', $outboundBytes);
if (!empty($project?->getId())) {
Span::add('realtime.projectId', $project->getId());
}
if (!empty($logUser?->getId())) {
Span::add('realtime.userId', $logUser->getId());
}
Span::current()?->finish();
}
});
$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId) {
$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId, $register) {
$project = null;
$authorization = null;
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
$rawSize = \strlen($message);
$messageType = 'invalid';
$subscriptionDelta = 0;
$subscriptionsRequested = 0;
$subscriptionsRemoved = 0;
$outboundBytes = 0;
$responseCode = 200;
$success = false;
Span::init('realtime.message');
Span::add('realtime.connectionId', $connection);
Span::add('realtime.inboundBytes', $rawSize);
Span::add('realtime.containerId', $containerId);
try {
$rawSize = \strlen($message);
$response = new Response(new SwooleResponse());
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
// Get authorization from connection (stored during onOpen)
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
@@ -883,6 +1011,12 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message format is not valid.');
}
$messageType = $message['type'] ?? 'invalid';
if (!\is_scalar($messageType)) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
}
// Ping does not require project context; other messages do (e.g. after unsubscribe during auth)
if (empty($projectId) && ($message['type'] ?? '') !== 'ping') {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing project context. Reconnect to the project first.');
@@ -895,6 +1029,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
]);
$server->send([$connection], $pongPayloadJson);
$outboundBytes += \strlen($pongPayloadJson);
if ($project !== null && !$project->isEmpty()) {
$pongOutboundBytes = \strlen($pongPayloadJson);
@@ -940,7 +1075,13 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
// Capture the pre-auth userId so we can rebind any account channels
// that were stored under it (e.g. guest who subscribed to `account`
// and now authenticates). unsubscribe() below clears the connection
// entry, so we must read it first.
$previousUserId = $realtime->connections[$connection]['userId'] ?? '';
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
$meta = $realtime->getSubscriptionMetadata($connection);
$realtime->unsubscribe($connection);
@@ -948,13 +1089,18 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
if (!empty($projectId)) {
foreach ($meta as $subscriptionId => $subscription) {
$queries = Query::parseQueries($subscription['queries'] ?? []);
$channels = Realtime::rebindAccountChannels(
$subscription['channels'] ?? [],
$previousUserId,
$user->getId()
);
$realtime->subscribe(
$projectId,
$connection,
$subscriptionId,
$roles,
$subscription['channels'] ?? [],
$channels,
$queries,
$user->getId()
);
@@ -965,6 +1111,12 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$realtime->connections[$connection]['authorization'] = $authorization;
}
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
if ($subscriptionDelta !== 0) {
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
}
$user = $response->output($user, Response::MODEL_ACCOUNT);
$authResponsePayloadJson = json_encode([
@@ -977,6 +1129,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
]);
$server->send([$connection], $authResponsePayloadJson);
$outboundBytes += \strlen($authResponsePayloadJson);
if ($project !== null && !$project->isEmpty()) {
$authOutboundBytes = \strlen($authResponsePayloadJson);
@@ -1009,6 +1162,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
// bulk validation + parsing before subscribing
$parsedPayloads = [];
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
foreach ($message['data'] as $payload) {
if (!\is_array($payload)) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Each subscribe payload must be an object.');
@@ -1037,22 +1191,28 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Invalid query: ' . $e->getMessage());
}
$convertedChannels = \array_keys(Realtime::convertChannels($payload['channels'], $userId));
$parsedPayloads[] = [
'subscriptionId' => $subscriptionId,
'channels' => $payload['channels'],
'convertedChannels' => $convertedChannels,
'queries' => $convertedQueries,
];
}
foreach ($parsedPayloads as $parsedPayload) {
$subscriptionId = $parsedPayload['subscriptionId'];
$channels = \array_keys(Realtime::convertChannels($parsedPayload['channels'], $userId));
$channels = $parsedPayload['convertedChannels'];
$queries = $parsedPayload['queries'];
$realtime->subscribe($projectId, $connection, $subscriptionId, $roles, $channels, $queries);
}
// subscribe() overwrites the connection entry; restore auth so later onMessage uses the same context.
$realtime->connections[$connection]['authorization'] = $authorization;
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
$subscriptionsRequested = \count($parsedPayloads);
if ($subscriptionDelta !== 0) {
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
}
$responsePayload = json_encode([
'type' => 'response',
@@ -1062,7 +1222,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
'subscriptions' => \array_map(function (array $parsedPayload) {
return [
'subscriptionId' => $parsedPayload['subscriptionId'],
'channels' => $parsedPayload['channels'],
'channels' => $parsedPayload['convertedChannels'],
'queries' => \array_map(fn ($q) => $q->toString(), $parsedPayload['queries']),
];
}, $parsedPayloads),
@@ -1070,6 +1230,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
]);
$server->send([$connection], $responsePayload);
$outboundBytes += \strlen($responsePayload);
if ($project !== null && !$project->isEmpty()) {
$subscribeOutboundBytes = \strlen($responsePayload);
@@ -1083,15 +1244,79 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
break;
case 'unsubscribe':
if (!\is_array($message['data']) || !\array_is_list($message['data'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
}
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
// Validate every payload before executing any removal so an invalid entry
// later in the batch does not leave earlier entries half-applied on the server.
$validatedIds = [];
foreach ($message['data'] as $payload) {
if (
!\is_array($payload)
|| !\array_key_exists('subscriptionId', $payload)
|| !\is_string($payload['subscriptionId'])
|| $payload['subscriptionId'] === ''
) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Each unsubscribe payload must include a non-empty subscriptionId.');
}
$validatedIds[] = $payload['subscriptionId'];
}
$unsubscribeResults = [];
foreach ($validatedIds as $subscriptionId) {
$wasRemoved = $realtime->unsubscribeSubscription($connection, $subscriptionId);
$unsubscribeResults[] = [
'subscriptionId' => $subscriptionId,
'removed' => $wasRemoved,
];
}
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
$subscriptionsRequested = \count($validatedIds);
$subscriptionsRemoved = \count(\array_filter($unsubscribeResults, fn (array $item) => $item['removed']));
if ($subscriptionDelta !== 0) {
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
}
$unsubscribeResponsePayload = json_encode([
'type' => 'response',
'data' => [
'to' => 'unsubscribe',
'success' => true,
'subscriptions' => $unsubscribeResults,
],
]);
$server->send([$connection], $unsubscribeResponsePayload);
$outboundBytes += \strlen($unsubscribeResponsePayload);
if ($project !== null && !$project->isEmpty()) {
$unsubscribeOutboundBytes = \strlen($unsubscribeResponsePayload);
if ($unsubscribeOutboundBytes > 0) {
triggerStats([
METRIC_REALTIME_OUTBOUND => $unsubscribeOutboundBytes,
], $project->getId());
}
}
break;
default:
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
}
$success = true;
} catch (Throwable $th) {
logError($th, 'realtimeMessage', project: $project, authorization: $authorization);
$code = $th->getCode();
if (!is_int($code)) {
$code = 500;
}
$responseCode = $code;
$message = $th->getMessage();
@@ -1108,19 +1333,52 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
]
];
$server->send([$connection], json_encode($response));
$responsePayloadJson = json_encode($response);
$server->send([$connection], $responsePayloadJson);
$outboundBytes += \strlen($responsePayloadJson);
if ($th->getCode() === 1008) {
$server->close($connection, $th->getCode());
}
Span::error($th);
} finally {
Span::add('realtime.success', $success);
Span::add('realtime.responseCode', $responseCode);
Span::add('realtime.subscriptionDelta', $subscriptionDelta);
Span::add('realtime.subscriptionsRequested', $subscriptionsRequested);
Span::add('realtime.subscriptionsRemoved', $subscriptionsRemoved);
Span::add('realtime.subscribe.subscriptionsCount', $subscriptionsRequested);
Span::add('realtime.outboundBytes', $outboundBytes);
Span::add('realtime.projectId', $project?->getId() ?? $projectId);
Span::add('realtime.userId', $realtime->connections[$connection]['userId'] ?? null);
Span::add('realtime.messageType', $messageType);
Span::current()?->finish();
}
});
$server->onClose(function (int $connection) use ($realtime, $stats, $register) {
$projectId = null;
$userId = null;
$subscriptionsBeforeClose = 0;
$success = false;
Span::init('realtime.close');
Span::add('realtime.connectionId', $connection);
if (array_key_exists($connection, $realtime->connections)) {
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
$userId = $realtime->connections[$connection]['userId'] ?? null;
}
try {
if (array_key_exists($connection, $realtime->connections)) {
$stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal');
$register->get('telemetry.connectionCounter')->add(-1);
$register->get('telemetry.workerClientCounter')->add(-1, $register->get('telemetry.workerAttributes'));
$subscriptionsBeforeClose = \count($realtime->getSubscriptionMetadata($connection));
if ($subscriptionsBeforeClose > 0) {
$register->get('telemetry.workerSubscriptionCounter')->add(-$subscriptionsBeforeClose, $register->get('telemetry.workerAttributes'));
}
$projectId = $realtime->connections[$connection]['projectId'];
@@ -1128,12 +1386,30 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) {
METRIC_REALTIME_CONNECTIONS => -1,
], $projectId);
}
$success = true;
} catch (\Throwable $th) {
// Log only; do not rethrow. If we let this bubble, Swoole dumps full coroutine
// backtraces and unsubscribe() below would never run (connection cleanup would fail).
Console::error('Realtime onClose error: ' . $th->getMessage());
Span::error($th);
} finally {
try {
$realtime->unsubscribe($connection);
} catch (\Throwable $th) {
Console::error('Realtime onClose unsubscribe error: ' . $th->getMessage());
Span::error($th);
}
Span::add('realtime.success', $success);
if (!empty($projectId)) {
Span::add('realtime.projectId', $projectId);
}
if (!empty($userId)) {
Span::add('realtime.userId', $userId);
}
Span::add('realtime.subscriptionsBeforeClose', $subscriptionsBeforeClose);
Span::current()?->finish();
}
$realtime->unsubscribe($connection);
Console::info('Connection close: ' . $connection);
});
+1 -22
View File
@@ -120,7 +120,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_SMTP_HOST
- _APP_SMTP_PORT
- _APP_SMTP_SECURE
@@ -256,7 +255,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_USAGE_STATS
- _APP_LOGGING_CONFIG
@@ -287,7 +285,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
appwrite-worker-webhooks:
@@ -315,7 +312,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -356,7 +352,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_STORAGE_DEVICE
- _APP_STORAGE_S3_ACCESS_KEY
- _APP_STORAGE_S3_SECRET
@@ -416,7 +411,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
appwrite-worker-builds:
@@ -453,7 +447,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_VCS_GITHUB_APP_NAME
- _APP_VCS_GITHUB_PRIVATE_KEY
@@ -529,7 +522,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
appwrite-worker-executions:
@@ -592,7 +584,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_FUNCTIONS_TIMEOUT
- _APP_SITES_TIMEOUT
- _APP_COMPUTE_BUILD_TIMEOUT
@@ -630,7 +621,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -673,7 +663,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_SMS_FROM
- _APP_SMS_PROVIDER
@@ -734,7 +723,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
@@ -773,7 +761,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
@@ -806,7 +793,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -839,7 +825,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -871,7 +856,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -907,7 +891,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
appwrite-task-scheduler-executions:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
@@ -936,7 +919,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
appwrite-task-scheduler-messages:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
@@ -965,7 +947,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
<?php if ($enableAssistant): ?>
appwrite-assistant:
@@ -1068,13 +1049,12 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
image: mongo:8.2.5
container_name: appwrite-mongodb
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
volumes:
- appwrite-mongodb:/data/db
- appwrite-mongodb-keyfile:/data/keyfile
ports:
- "27017:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=root
- MONGO_INITDB_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
@@ -1205,7 +1185,6 @@ volumes:
<?php elseif ($dbService === 'mongodb'): ?>
appwrite-mongodb:
appwrite-mongodb-keyfile:
appwrite-mongodb-config:
<?php endif; ?>
appwrite-redis:
appwrite-cache:
+1 -1
View File
@@ -129,7 +129,7 @@ $worker
$log->setAction('appwrite-queue-' . $queueName);
$log->addTag('verboseType', get_class($error));
$log->addTag('code', $error->getCode());
$log->addTag('projectId', $project->getId() ?? 'n/a');
$log->addTag('projectId', $project->getId());
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
+3 -2
View File
@@ -69,12 +69,13 @@
"utopia-php/dsn": "0.2.1",
"utopia-php/http": "0.34.*",
"utopia-php/fetch": "0.5.*",
"utopia-php/validators": "0.2.*",
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.22.*",
"utopia-php/migration": "1.9.*",
"utopia-php/platform": "0.12.*",
"utopia-php/platform": "0.13.*",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
"utopia-php/preloader": "0.2.*",
@@ -92,7 +93,7 @@
"chillerlan/php-qrcode": "4.3.*",
"adhocore/jwt": "1.1.*",
"spomky-labs/otphp": "11.*",
"webonyx/graphql-php": "14.11.*",
"webonyx/graphql-php": "15.31.*",
"league/csv": "9.14.*",
"enshrined/svg-sanitize": "0.22.*"
},
Generated
+173 -156
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "4fb974e9843f6104e40396e7cad4a833",
"content-hash": "805802552f7482eaeae4bdaa505ae982",
"packages": [
{
"name": "adhocore/jwt",
@@ -1996,16 +1996,16 @@
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.51",
"version": "3.0.52",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "d59c94077f9c9915abb51ddb52ce85188ece1748"
"reference": "2adaefc83df2ec548558307690f376dd7d4f4fce"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d59c94077f9c9915abb51ddb52ce85188ece1748",
"reference": "d59c94077f9c9915abb51ddb52ce85188ece1748",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2adaefc83df2ec548558307690f376dd7d4f4fce",
"reference": "2adaefc83df2ec548558307690f376dd7d4f4fce",
"shasum": ""
},
"require": {
@@ -2086,7 +2086,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.51"
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.52"
},
"funding": [
{
@@ -2102,7 +2102,7 @@
"type": "tidelift"
}
],
"time": "2026-04-10T01:33:53+00:00"
"time": "2026-04-27T07:02:15+00:00"
},
{
"name": "psr/clock",
@@ -2887,7 +2887,7 @@
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.34.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
@@ -2948,7 +2948,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0"
},
"funding": [
{
@@ -2972,7 +2972,7 @@
},
{
"name": "symfony/polyfill-php82",
"version": "v1.34.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php82.git",
@@ -3028,7 +3028,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php82/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-php82/tree/v1.37.0"
},
"funding": [
{
@@ -3052,7 +3052,7 @@
},
{
"name": "symfony/polyfill-php83",
"version": "v1.34.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
@@ -3108,7 +3108,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php83/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0"
},
"funding": [
{
@@ -3132,16 +3132,16 @@
},
{
"name": "symfony/polyfill-php85",
"version": "v1.34.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php85.git",
"reference": "2c408a6bb0313e6001a83628dc5506100474254e"
"reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/2c408a6bb0313e6001a83628dc5506100474254e",
"reference": "2c408a6bb0313e6001a83628dc5506100474254e",
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee",
"reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee",
"shasum": ""
},
"require": {
@@ -3188,7 +3188,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php85/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0"
},
"funding": [
{
@@ -3208,7 +3208,7 @@
"type": "tidelift"
}
],
"time": "2026-04-10T16:50:15+00:00"
"time": "2026-04-26T13:10:57+00:00"
},
{
"name": "symfony/service-contracts",
@@ -3658,16 +3658,16 @@
},
{
"name": "utopia-php/cli",
"version": "0.23.1",
"version": "0.23.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/cli.git",
"reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621"
"reference": "145b91fef827853bcceaa3ab8ca2b1d6faaca2ab"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/cli/zipball/8d1955b8bc4dc631f45d7c7df689ed7b63f70621",
"reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621",
"url": "https://api.github.com/repos/utopia-php/cli/zipball/145b91fef827853bcceaa3ab8ca2b1d6faaca2ab",
"reference": "145b91fef827853bcceaa3ab8ca2b1d6faaca2ab",
"shasum": ""
},
"require": {
@@ -3703,9 +3703,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/cli/issues",
"source": "https://github.com/utopia-php/cli/tree/0.23.1"
"source": "https://github.com/utopia-php/cli/tree/0.23.2"
},
"time": "2026-04-05T15:27:35+00:00"
"time": "2026-04-27T09:19:04+00:00"
},
{
"name": "utopia-php/compression",
@@ -3850,16 +3850,16 @@
},
{
"name": "utopia-php/database",
"version": "5.3.20",
"version": "5.4.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24"
"reference": "7479a74d4e7c67a58689c0f45bf64b7c403ad54e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24",
"reference": "fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24",
"url": "https://api.github.com/repos/utopia-php/database/zipball/7479a74d4e7c67a58689c0f45bf64b7c403ad54e",
"reference": "7479a74d4e7c67a58689c0f45bf64b7c403ad54e",
"shasum": ""
},
"require": {
@@ -3903,9 +3903,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/5.3.20"
"source": "https://github.com/utopia-php/database/tree/5.4.0"
},
"time": "2026-04-10T08:27:41+00:00"
"time": "2026-04-29T06:30:39+00:00"
},
{
"name": "utopia-php/detector",
@@ -4271,21 +4271,20 @@
},
{
"name": "utopia-php/http",
"version": "0.34.20",
"version": "0.34.24",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/http.git",
"reference": "d6b360d555022d16c16d40be51f86180364819f8"
"reference": "d1eced0627c5a9fceddf53992ed97d664b810d33"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/http/zipball/d6b360d555022d16c16d40be51f86180364819f8",
"reference": "d6b360d555022d16c16d40be51f86180364819f8",
"url": "https://api.github.com/repos/utopia-php/http/zipball/d1eced0627c5a9fceddf53992ed97d664b810d33",
"reference": "d1eced0627c5a9fceddf53992ed97d664b810d33",
"shasum": ""
},
"require": {
"ext-swoole": "*",
"php": ">=8.2",
"php": ">=8.3",
"utopia-php/compression": "0.1.*",
"utopia-php/di": "0.3.*",
"utopia-php/servers": "0.3.*",
@@ -4295,11 +4294,14 @@
"require-dev": {
"doctrine/instantiator": "^1.5",
"laravel/pint": "1.*",
"phpbench/phpbench": "^1.2",
"phpstan/phpstan": "1.*",
"phpunit/phpunit": "^9.5.25",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^12.0",
"rector/rector": "^2.4",
"swoole/ide-helper": "4.8.3"
},
"suggest": {
"ext-swoole": "Required to use the Swoole server adapter (\\Utopia\\Http\\Adapter\\Swoole\\Server)."
},
"type": "library",
"autoload": {
"psr-4": {
@@ -4319,22 +4321,22 @@
],
"support": {
"issues": "https://github.com/utopia-php/http/issues",
"source": "https://github.com/utopia-php/http/tree/0.34.20"
"source": "https://github.com/utopia-php/http/tree/0.34.24"
},
"time": "2026-04-12T14:25:22+00:00"
"time": "2026-04-24T12:16:53+00:00"
},
{
"name": "utopia-php/image",
"version": "0.8.4",
"version": "0.8.6",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/image.git",
"reference": "ce788ff0121a79286fdbe3ef3eba566de646df65"
"reference": "85ab7027873e11bc901110d8f7830252247ba724"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/image/zipball/ce788ff0121a79286fdbe3ef3eba566de646df65",
"reference": "ce788ff0121a79286fdbe3ef3eba566de646df65",
"url": "https://api.github.com/repos/utopia-php/image/zipball/85ab7027873e11bc901110d8f7830252247ba724",
"reference": "85ab7027873e11bc901110d8f7830252247ba724",
"shasum": ""
},
"require": {
@@ -4343,10 +4345,12 @@
"php": ">=8.1"
},
"require-dev": {
"laravel/pint": "1.2.*",
"phpstan/phpstan": "^1.10.0",
"phpunit/phpunit": "^9.3",
"vimeo/psalm": "4.13.1"
"laravel/pint": "1.24.*",
"phpstan/phpstan": "2.1.*",
"phpunit/phpunit": "10.5.*"
},
"suggest": {
"ext-imagick": "Imagick extension is required for Imagick adapter"
},
"type": "library",
"autoload": {
@@ -4368,9 +4372,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/image/issues",
"source": "https://github.com/utopia-php/image/tree/0.8.4"
"source": "https://github.com/utopia-php/image/tree/0.8.6"
},
"time": "2025-06-03T08:32:20+00:00"
"time": "2026-04-19T12:52:59+00:00"
},
{
"name": "utopia-php/locale",
@@ -4526,16 +4530,16 @@
},
{
"name": "utopia-php/migration",
"version": "1.9.1",
"version": "1.9.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "7a86aeadf182b63a9f4ceba7e137588b31c5d2e2"
"reference": "111f6221d04578a6f721c23ac872002375f176ae"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/7a86aeadf182b63a9f4ceba7e137588b31c5d2e2",
"reference": "7a86aeadf182b63a9f4ceba7e137588b31c5d2e2",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/111f6221d04578a6f721c23ac872002375f176ae",
"reference": "111f6221d04578a6f721c23ac872002375f176ae",
"shasum": ""
},
"require": {
@@ -4575,22 +4579,22 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/1.9.1"
"source": "https://github.com/utopia-php/migration/tree/1.9.3"
},
"time": "2026-03-25T07:05:27+00:00"
"time": "2026-04-22T07:13:26+00:00"
},
{
"name": "utopia-php/mongo",
"version": "1.0.2",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/mongo.git",
"reference": "677a21c53f7a1316c528b4b45b3fce886cee7223"
"reference": "73593682deee4696525a04e26524c1c1226e1530"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/mongo/zipball/677a21c53f7a1316c528b4b45b3fce886cee7223",
"reference": "677a21c53f7a1316c528b4b45b3fce886cee7223",
"url": "https://api.github.com/repos/utopia-php/mongo/zipball/73593682deee4696525a04e26524c1c1226e1530",
"reference": "73593682deee4696525a04e26524c1c1226e1530",
"shasum": ""
},
"require": {
@@ -4636,22 +4640,22 @@
],
"support": {
"issues": "https://github.com/utopia-php/mongo/issues",
"source": "https://github.com/utopia-php/mongo/tree/1.0.2"
"source": "https://github.com/utopia-php/mongo/tree/1.1.0"
},
"time": "2026-03-18T02:45:50+00:00"
"time": "2026-04-24T06:15:10+00:00"
},
{
"name": "utopia-php/platform",
"version": "0.12.1",
"version": "0.13.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/platform.git",
"reference": "2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc"
"reference": "d23af5349a7ea9ee11f9920a13626226f985522e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc",
"reference": "2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/d23af5349a7ea9ee11f9920a13626226f985522e",
"reference": "d23af5349a7ea9ee11f9920a13626226f985522e",
"shasum": ""
},
"require": {
@@ -4687,9 +4691,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/platform/issues",
"source": "https://github.com/utopia-php/platform/tree/0.12.1"
"source": "https://github.com/utopia-php/platform/tree/0.13.0"
},
"time": "2026-04-08T04:11:31+00:00"
"time": "2026-04-17T09:57:18+00:00"
},
{
"name": "utopia-php/pools",
@@ -5180,16 +5184,16 @@
},
{
"name": "utopia-php/validators",
"version": "0.2.0",
"version": "0.2.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/validators.git",
"reference": "30b6030a5b100fc1dff34506e5053759594b2a20"
"reference": "5d7d494e64457cd4eb67fdcfd9481f2c89796aa6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/validators/zipball/30b6030a5b100fc1dff34506e5053759594b2a20",
"reference": "30b6030a5b100fc1dff34506e5053759594b2a20",
"url": "https://api.github.com/repos/utopia-php/validators/zipball/5d7d494e64457cd4eb67fdcfd9481f2c89796aa6",
"reference": "5d7d494e64457cd4eb67fdcfd9481f2c89796aa6",
"shasum": ""
},
"require": {
@@ -5219,9 +5223,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/validators/issues",
"source": "https://github.com/utopia-php/validators/tree/0.2.0"
"source": "https://github.com/utopia-php/validators/tree/0.2.2"
},
"time": "2026-01-13T09:16:51+00:00"
"time": "2026-04-27T16:30:24+00:00"
},
{
"name": "utopia-php/vcs",
@@ -5381,38 +5385,48 @@
},
{
"name": "webonyx/graphql-php",
"version": "v14.11.10",
"version": "v15.31.5",
"source": {
"type": "git",
"url": "https://github.com/webonyx/graphql-php.git",
"reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19"
"reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/d9c2fdebc6aa01d831bc2969da00e8588cffef19",
"reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19",
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/089c4ef7e112df85788cfe06596278a8f99f4aa9",
"reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"php": "^7.1 || ^8"
"php": "^7.4 || ^8"
},
"require-dev": {
"amphp/amp": "^2.3",
"doctrine/coding-standard": "^6.0",
"nyholm/psr7": "^1.2",
"amphp/amp": "^2.6",
"amphp/http-server": "^2.1",
"dms/phpunit-arraysubset-asserts": "dev-master",
"ergebnis/composer-normalize": "^2.28",
"friendsofphp/php-cs-fixer": "3.94.2",
"mll-lab/php-cs-fixer-config": "5.13.0",
"nyholm/psr7": "^1.5",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "0.12.82",
"phpstan/phpstan-phpunit": "0.12.18",
"phpstan/phpstan-strict-rules": "0.12.9",
"phpunit/phpunit": "^7.2 || ^8.5",
"psr/http-message": "^1.0",
"react/promise": "2.*",
"simpod/php-coveralls-mirror": "^3.0"
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "2.1.46",
"phpstan/phpstan-phpunit": "2.0.16",
"phpstan/phpstan-strict-rules": "2.0.10",
"phpunit/phpunit": "^9.5 || ^10.5.21 || ^11",
"psr/http-message": "^1 || ^2",
"react/http": "^1.6",
"react/promise": "^2.0 || ^3.0",
"rector/rector": "^2.0",
"symfony/polyfill-php81": "^1.23",
"symfony/var-exporter": "^5 || ^6 || ^7 || ^8",
"thecodingmachine/safe": "^1.3 || ^2 || ^3",
"ticketswap/phpstan-error-formatter": "1.3.0"
},
"suggest": {
"amphp/http-server": "To leverage async resolving with webserver on AMPHP platform",
"psr/http-message": "To use standard GraphQL server",
"react/promise": "To leverage async resolving on React PHP platform"
},
@@ -5434,30 +5448,34 @@
],
"support": {
"issues": "https://github.com/webonyx/graphql-php/issues",
"source": "https://github.com/webonyx/graphql-php/tree/v14.11.10"
"source": "https://github.com/webonyx/graphql-php/tree/v15.31.5"
},
"funding": [
{
"url": "https://github.com/spawnia",
"type": "github"
},
{
"url": "https://opencollective.com/webonyx-graphql-php",
"type": "open_collective"
}
],
"time": "2023-07-05T14:23:37+00:00"
"time": "2026-04-11T18:06:15+00:00"
}
],
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.17.11",
"version": "1.24.0",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "c714ee52659ef5968b3372ff4da0e407140a6250"
"reference": "6d4d26659bc7a1c347c1d4d8dae3b77b5562e0cb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/c714ee52659ef5968b3372ff4da0e407140a6250",
"reference": "c714ee52659ef5968b3372ff4da0e407140a6250",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6d4d26659bc7a1c347c1d4d8dae3b77b5562e0cb",
"reference": "6d4d26659bc7a1c347c1d4d8dae3b77b5562e0cb",
"shasum": ""
},
"require": {
@@ -5493,9 +5511,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.17.11"
"source": "https://github.com/appwrite/sdk-generator/tree/1.24.0"
},
"time": "2026-04-11T02:42:32+00:00"
"time": "2026-04-24T12:50:05+00:00"
},
{
"name": "brianium/paratest",
@@ -5777,16 +5795,16 @@
},
{
"name": "laravel/pint",
"version": "v1.29.0",
"version": "v1.29.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
"reference": "bdec963f53172c5e36330f3a400604c69bf02d39"
"reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39",
"reference": "bdec963f53172c5e36330f3a400604c69bf02d39",
"url": "https://api.github.com/repos/laravel/pint/zipball/0770e9b7fafd50d4586881d456d6eb41c9247a80",
"reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80",
"shasum": ""
},
"require": {
@@ -5797,14 +5815,14 @@
"php": "^8.2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.94.2",
"illuminate/view": "^12.54.1",
"larastan/larastan": "^3.9.3",
"laravel-zero/framework": "^12.0.5",
"friendsofphp/php-cs-fixer": "^3.95.1",
"illuminate/view": "^12.56.0",
"larastan/larastan": "^3.9.6",
"laravel-zero/framework": "^12.1.0",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.4.0",
"pestphp/pest": "^3.8.6",
"shipfastlabs/agent-detector": "^1.1.0"
"shipfastlabs/agent-detector": "^1.1.3"
},
"bin": [
"builds/pint"
@@ -5841,7 +5859,7 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
"time": "2026-03-12T15:51:39+00:00"
"time": "2026-04-20T15:26:14+00:00"
},
{
"name": "matthiasmullie/minify",
@@ -6204,11 +6222,11 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.46",
"version": "2.1.51",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
"reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc3b523c45e714c70de2ac5113b958223b55dc59",
"reference": "dc3b523c45e714c70de2ac5113b958223b55dc59",
"shasum": ""
},
"require": {
@@ -6253,20 +6271,20 @@
"type": "github"
}
],
"time": "2026-04-01T09:25:14+00:00"
"time": "2026-04-21T18:22:01+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "12.5.3",
"version": "12.5.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d"
"reference": "876099a072646c7745f673d7aeab5382c4439691"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b015312f28dd75b75d3422ca37dff2cd1a565e8d",
"reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/876099a072646c7745f673d7aeab5382c4439691",
"reference": "876099a072646c7745f673d7aeab5382c4439691",
"shasum": ""
},
"require": {
@@ -6275,7 +6293,6 @@
"ext-xmlwriter": "*",
"nikic/php-parser": "^5.7.0",
"php": ">=8.3",
"phpunit/php-file-iterator": "^6.0",
"phpunit/php-text-template": "^5.0",
"sebastian/complexity": "^5.0",
"sebastian/environment": "^8.0.3",
@@ -6322,7 +6339,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.3"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.6"
},
"funding": [
{
@@ -6342,7 +6359,7 @@
"type": "tidelift"
}
],
"time": "2026-02-06T06:01:44+00:00"
"time": "2026-04-15T08:23:17+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -6603,16 +6620,16 @@
},
{
"name": "phpunit/phpunit",
"version": "12.5.17",
"version": "12.5.23",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "85b62adab1a340982df64e66daa4a4435eb5723b"
"reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/85b62adab1a340982df64e66daa4a4435eb5723b",
"reference": "85b62adab1a340982df64e66daa4a4435eb5723b",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969",
"reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969",
"shasum": ""
},
"require": {
@@ -6626,15 +6643,15 @@
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.3",
"phpunit/php-code-coverage": "^12.5.3",
"phpunit/php-code-coverage": "^12.5.6",
"phpunit/php-file-iterator": "^6.0.1",
"phpunit/php-invoker": "^6.0.0",
"phpunit/php-text-template": "^5.0.0",
"phpunit/php-timer": "^8.0.0",
"sebastian/cli-parser": "^4.2.0",
"sebastian/comparator": "^7.1.4",
"sebastian/comparator": "^7.1.6",
"sebastian/diff": "^7.0.0",
"sebastian/environment": "^8.0.4",
"sebastian/environment": "^8.1.0",
"sebastian/exporter": "^7.0.2",
"sebastian/global-state": "^8.0.2",
"sebastian/object-enumerator": "^7.0.0",
@@ -6681,7 +6698,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.17"
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.23"
},
"funding": [
{
@@ -6689,7 +6706,7 @@
"type": "other"
}
],
"time": "2026-04-08T03:04:19+00:00"
"time": "2026-04-18T06:12:49+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -6762,16 +6779,16 @@
},
{
"name": "sebastian/comparator",
"version": "7.1.5",
"version": "7.1.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "c284f55811f43d555e51e8e5c166ac40d3e33c63"
"reference": "c769009dee98f494e0edc3fd4f4087501688f11e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c284f55811f43d555e51e8e5c166ac40d3e33c63",
"reference": "c284f55811f43d555e51e8e5c166ac40d3e33c63",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c769009dee98f494e0edc3fd4f4087501688f11e",
"reference": "c769009dee98f494e0edc3fd4f4087501688f11e",
"shasum": ""
},
"require": {
@@ -6830,7 +6847,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
"source": "https://github.com/sebastianbergmann/comparator/tree/7.1.5"
"source": "https://github.com/sebastianbergmann/comparator/tree/7.1.6"
},
"funding": [
{
@@ -6850,7 +6867,7 @@
"type": "tidelift"
}
],
"time": "2026-04-08T04:43:00+00:00"
"time": "2026-04-14T08:23:15+00:00"
},
{
"name": "sebastian/complexity",
@@ -6979,16 +6996,16 @@
},
{
"name": "sebastian/environment",
"version": "8.0.4",
"version": "8.1.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
"reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11"
"reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7b8842c2d8e85d0c3a5831236bf5869af6ab2a11",
"reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/b121608b28a13f721e76ffbbd386d08eff58f3f6",
"reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6",
"shasum": ""
},
"require": {
@@ -7003,7 +7020,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "8.0-dev"
"dev-main": "8.1-dev"
}
},
"autoload": {
@@ -7031,7 +7048,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
"security": "https://github.com/sebastianbergmann/environment/security/policy",
"source": "https://github.com/sebastianbergmann/environment/tree/8.0.4"
"source": "https://github.com/sebastianbergmann/environment/tree/8.1.0"
},
"funding": [
{
@@ -7051,7 +7068,7 @@
"type": "tidelift"
}
],
"time": "2026-03-15T07:05:40+00:00"
"time": "2026-04-15T12:13:01+00:00"
},
{
"name": "sebastian/exporter",
@@ -7764,7 +7781,7 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.34.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
@@ -7823,7 +7840,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0"
},
"funding": [
{
@@ -7847,16 +7864,16 @@
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.34.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df"
"reference": "4864388bfbd3001ce88e234fab652acd91fdc57e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
"reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e",
"reference": "4864388bfbd3001ce88e234fab652acd91fdc57e",
"shasum": ""
},
"require": {
@@ -7905,7 +7922,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0"
},
"funding": [
{
@@ -7925,11 +7942,11 @@
"type": "tidelift"
}
],
"time": "2026-04-10T16:19:22+00:00"
"time": "2026-04-26T13:13:48+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.34.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@@ -7990,7 +8007,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0"
},
"funding": [
{
@@ -8014,7 +8031,7 @@
},
{
"name": "symfony/polyfill-php81",
"version": "v1.34.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
@@ -8070,7 +8087,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-php81/tree/v1.37.0"
},
"funding": [
{
@@ -8449,5 +8466,5 @@
"platform-dev": {
"ext-fileinfo": "*"
},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}
+4 -3
View File
@@ -242,19 +242,20 @@ services:
- _APP_EXPERIMENT_LOGGING_PROVIDER
- _APP_EXPERIMENT_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
- _APP_DATABASE_SHARED_TABLES_V1
- _APP_DATABASE_SHARED_NAMESPACE
- _APP_FUNCTIONS_CREATION_ABUSE_LIMIT
- _APP_CUSTOM_DOMAIN_DENY_LIST
- _APP_TRUSTED_HEADERS
- _APP_MIGRATION_HOST
- _TESTS_OAUTH2_GITHUB_CLIENT_ID
- _TESTS_OAUTH2_GITHUB_CLIENT_SECRET
extra_hosts:
- "host.docker.internal:host-gateway"
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: appwrite/console:7.8.26
image: appwrite/console:7.8.45
restart: unless-stopped
networks:
- appwrite
@@ -462,7 +463,6 @@ services:
- _APP_EXECUTOR_SECRET
- _APP_EXECUTOR_HOST
- _APP_DATABASE_SHARED_TABLES
- _APP_DATABASE_SHARED_TABLES_V1
- _APP_EMAIL_CERTIFICATES
- _APP_MAINTENANCE_RETENTION_AUDIT
- _APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE
@@ -1288,6 +1288,7 @@ services:
image: mongo:8.2.5
container_name: appwrite-mongodb
<<: *x-logging
restart: on-failure:3
networks:
- appwrite
volumes:
@@ -1 +0,0 @@
Initialize an MFA challenge of the specified factor. The factor must be available on the account.
@@ -1 +0,0 @@
Use this endpoint to log out the currently logged in user from their account. When successful this endpoint will delete the user session and remove the session secret cookie from the user client.
@@ -0,0 +1 @@
List all OAuth2 providers supported by the Appwrite server, along with the parameters required to configure each provider. The response excludes mock providers but includes sandbox providers.
@@ -1 +0,0 @@
Get the collection activity logs list by its unique ID.
@@ -1 +0,0 @@
Get the document activity logs list by its unique ID.
@@ -1 +0,0 @@
List attributes in the collection.
@@ -1 +0,0 @@
Create a new build for an existing function deployment. This endpoint allows you to rebuild a deployment with the updated function configuration, including its entrypoint and build commands if they have been modified. The build process will be queued and executed asynchronously. The original deployment's code will be preserved and used for the new build.
@@ -1,5 +0,0 @@
Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.
This endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https://appwrite.io/docs/functions).
Use the "command" param to set the entrypoint used to execute your code.
@@ -1 +0,0 @@
Trigger a function execution. The returned object will return you the current execution status. You can ping the `Get Execution` endpoint to get updates on the current execution status. Once this endpoint is called, your function execution process will start asynchronously.
@@ -1 +0,0 @@
Create a new function. You can pass a list of [permissions](https://appwrite.io/docs/permissions) to allow different project users or team with access to execute the function using the client API.
@@ -1 +0,0 @@
Create a new function environment variable. These variables can be accessed in the function at runtime as environment variables.
@@ -1 +0,0 @@
Delete a code deployment by its unique ID.
@@ -1 +0,0 @@
Delete a function execution by its unique ID.
@@ -1 +0,0 @@
Delete a function by its unique ID.
@@ -1 +0,0 @@
Delete a variable by its unique ID.
@@ -1 +0,0 @@
Get a Deployment's contents by its unique ID. This endpoint supports range requests for partial or streaming file download.
@@ -1 +0,0 @@
Get a code deployment by its unique ID.
@@ -1 +0,0 @@
Get a function execution log by its unique ID.
@@ -1 +0,0 @@
Get usage metrics and statistics for a for a specific function. View statistics including total deployments, builds, executions, storage usage, and compute time. The response includes both current totals and historical data for each metric. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, defaults to 30 days.
@@ -1 +0,0 @@
Get a function by its unique ID.
@@ -1 +0,0 @@
Get usage metrics and statistics for a for all functions. View statistics including total functions, deployments, builds, executions, storage usage, and compute time. The response includes both current totals and historical data for each metric. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, defaults to 30 days.
@@ -1 +0,0 @@
Get a function template using ID. You can use template details in [createFunction](/docs/references/cloud/server-nodejs/functions#create) method.
@@ -1 +0,0 @@
Get a variable by its unique ID.
@@ -1 +0,0 @@
Get a list of all the function's code deployments. You can use the query params to filter your results.
@@ -1 +0,0 @@
Get a list of all the current user function execution logs. You can use the query params to filter your results.
@@ -1 +0,0 @@
Get a list of all the project's functions. You can use the query params to filter your results.
@@ -1 +0,0 @@
Get a list of all runtimes that are currently active on your instance.
@@ -1 +0,0 @@
List allowed function specifications for this instance.
@@ -1 +0,0 @@
List available function templates. You can use template details in [createFunction](/docs/references/cloud/server-nodejs/functions#create) method.
@@ -1 +0,0 @@
Get a list of all variables of a specific function.
@@ -1 +0,0 @@
Cancel an ongoing function deployment build. If the build is already in progress, it will be stopped and marked as canceled. If the build hasn't started yet, it will be marked as canceled without executing. You cannot cancel builds that have already completed (status 'ready') or failed. The response includes the final build status and details.
@@ -1 +0,0 @@
Update the function code deployment ID using the unique function ID. Use this endpoint to switch the code deployment that should be executed by the execution endpoint.
@@ -1 +0,0 @@
Update function by its unique ID.
@@ -1 +0,0 @@
Update variable by its unique ID.
@@ -1 +0,0 @@
Get the number of projects containing metrics that are waiting to be processed in the Appwrite internal queue server.
@@ -1 +0,0 @@
Get the number of tasks that are waiting to be processed in the Appwrite internal queue server.
-1
View File
@@ -1 +0,0 @@
Check the Appwrite queue messaging servers are up and connection is successful.
-1
View File
@@ -1 +0,0 @@
Delete a message by its unique ID.
@@ -1 +0,0 @@
Create a new project variable. This variable will be accessible in all Appwrite Functions at runtime.
@@ -1 +0,0 @@
Delete a project variable by its unique ID.
-1
View File
@@ -1 +0,0 @@
Get a project variable by its unique ID.
@@ -1 +0,0 @@
Get a list of all project variables. These variables will be accessible in all Appwrite Functions at runtime.
@@ -1 +0,0 @@
Update project variable by its unique ID. This variable will be accessible in all Appwrite Functions at runtime.
-1
View File
@@ -1 +0,0 @@
Create a new API key. It's recommended to have multiple API keys with strict scopes for separate functions within your project.
@@ -1 +0,0 @@
Create a new platform for your project. Use this endpoint to register a new platform where your users will run your application which will interact with the Appwrite API.
@@ -1 +0,0 @@
Create a new webhook. Use this endpoint to configure a URL that will receive events from Appwrite when specific events occur.
-1
View File
@@ -1 +0,0 @@
Delete a key by its unique ID. Once deleted, the key can no longer be used to authenticate API calls.
@@ -1 +0,0 @@
Delete a platform by its unique ID. This endpoint removes the platform and all its configurations from the project.
@@ -1 +0,0 @@
Reset a custom SMS template to its default value. This endpoint removes any custom message and restores the template to its original state.
@@ -1 +0,0 @@
Delete a webhook by its unique ID. Once deleted, the webhook will no longer receive project events.
-1
View File
@@ -1 +0,0 @@
Get a key by its unique ID. This endpoint returns details about a specific API key in your project including it's scopes.
-1
View File
@@ -1 +0,0 @@
Get a platform by its unique ID. This endpoint returns the platform's details, including its name, type, and key configurations.
@@ -1 +0,0 @@
Get a custom SMS template for the specified locale and type returning it's contents.
-1
View File
@@ -1 +0,0 @@
Get a webhook by its unique ID. This endpoint returns details about a specific webhook configured for a project.
-1
View File
@@ -1 +0,0 @@
Get a list of all API keys from the current project.
@@ -1 +0,0 @@
Get a list of all platforms in the project. This endpoint returns an array of all platforms and their configurations.
@@ -1 +0,0 @@
Get a list of all webhooks belonging to the project. You can use the query params to filter your results.
@@ -1 +0,0 @@
Update the status of all API types. Use this endpoint to enable or disable API types such as REST, GraphQL and Realtime all at once.
@@ -1 +0,0 @@
Update the status of a specific API type. Use this endpoint to enable or disable API types such as REST, GraphQL and Realtime.
@@ -1 +0,0 @@
Update how long sessions created within a project should stay active for.

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