From 8f176166c9f1e24eb7d7bf2124e2f725bbcf5b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 Apr 2026 15:31:10 +0200 Subject: [PATCH 01/11] Re-introduce project JWT endpoint --- app/controllers/api/projects.php | 44 ++++++++++++++++ .../Projects/ProjectsConsoleClientTest.php | 52 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 494aa11150..da772d6dbb 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1,5 +1,6 @@ dynamic($project, Response::MODEL_PROJECT); }); +// JWT Keys + +Http::post('/v1/projects/:projectId/jwts') + ->groups(['api', 'projects']) + ->desc('Create JWT') + ->label('scope', 'projects.write') + ->label('sdk', new Method( + namespace: 'projects', + group: 'auth', + name: 'createJWT', + description: '/docs/references/projects/create-jwt.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_CREATED, + model: Response::MODEL_JWT, + ) + ] + )) + ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) + ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for JWT key. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.') + ->param('duration', 900, new Range(0, 3600), 'Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.', true) + ->inject('response') + ->inject('dbForPlatform') + ->action(function (string $projectId, array $scopes, int $duration, Response $response, Database $dbForPlatform) { + + $project = $dbForPlatform->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $duration, 0); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic(new Document(['jwt' => API_KEY_DYNAMIC . '_' . $jwt->encode([ + 'projectId' => $project->getId(), + 'scopes' => $scopes + ])]), Response::MODEL_JWT); + }); + // Backwards compatibility Http::patch('/v1/projects/:projectId/oauth2') ->desc('Update project OAuth2') diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 8322e37de1..d71537d534 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -3941,6 +3941,58 @@ class ProjectsConsoleClientTest extends Scope $this->assertEmpty($response['body']); } + // JWT Keys + + public function testJWTKey(): void + { + $data = $this->setupProjectData(); + $id = $data['projectId']; + + // Create JWT key + $response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/jwts', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'duration' => 5, + 'scopes' => ['users.read'], + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['jwt']); + + $jwt = $response['body']['jwt']; + + // Ensure JWT key works + $response = $this->client->call(Client::METHOD_GET, '/users', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $id, + 'x-appwrite-key' => $jwt, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertArrayHasKey('users', $response['body']); + + // Ensure JWT key respect scopes + $response = $this->client->call(Client::METHOD_GET, '/functions', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $id, + 'x-appwrite-key' => $jwt, + ]); + + $this->assertEquals(401, $response['headers']['status-code']); + + // Ensure JWT key expires + \sleep(10); + + $response = $this->client->call(Client::METHOD_GET, '/users', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $id, + 'x-appwrite-key' => $jwt, + ]); + + $this->assertEquals(401, $response['headers']['status-code']); + } + // Platforms public function testCreateProjectPlatform(): void From ed9b47f6ce7d8aff0d1962df7f1e65a293ac1e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 Apr 2026 15:57:37 +0200 Subject: [PATCH 02/11] Migrate project jwt to dynamic api key --- app/controllers/api/projects.php | 42 ------- .../Http/Project/Keys/Dynamic/Create.php | 115 ++++++++++++++++++ .../Project/Keys/{ => Standard}/Create.php | 15 ++- .../Projects/ProjectsConsoleClientTest.php | 2 + 4 files changed, 126 insertions(+), 48 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Dynamic/Create.php rename src/Appwrite/Platform/Modules/Project/Http/Project/Keys/{ => Standard}/Create.php (88%) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index da772d6dbb..ca7f8bb216 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -60,48 +60,6 @@ Http::get('/v1/projects/:projectId') $response->dynamic($project, Response::MODEL_PROJECT); }); -// JWT Keys - -Http::post('/v1/projects/:projectId/jwts') - ->groups(['api', 'projects']) - ->desc('Create JWT') - ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'auth', - name: 'createJWT', - description: '/docs/references/projects/create-jwt.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_JWT, - ) - ] - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for JWT key. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.') - ->param('duration', 900, new Range(0, 3600), 'Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.', true) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, array $scopes, int $duration, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $duration, 0); - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic(new Document(['jwt' => API_KEY_DYNAMIC . '_' . $jwt->encode([ - 'projectId' => $project->getId(), - 'scopes' => $scopes - ])]), Response::MODEL_JWT); - }); - // Backwards compatibility Http::patch('/v1/projects/:projectId/oauth2') ->desc('Update project OAuth2') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Dynamic/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Dynamic/Create.php new file mode 100644 index 0000000000..2df1f2a303 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Dynamic/Create.php @@ -0,0 +1,115 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/project/keys/dynamic') + ->httpAlias('/v1/projects/:projectId/jwts') + ->desc('Create dynamic project key') + ->groups(['api', 'project']) + ->label('scope', 'keys.write') + ->label('event', 'keys.[keyId].create') + ->label('audits.event', 'project.key.create') + ->label('audits.resource', 'project.key/{response.$id}') + ->label('sdk', new Method( + namespace: 'project', + group: 'keys', + name: 'createDynamicKey', + description: <<param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', optional: false) + ->param('duration', 900, new Range(1, 3600), 'Time in seconds before dynamic key expires. Default duration is 900 seconds, and maximum is 3600 seconds.', true) + ->inject('response') + ->inject('queueForEvents') + ->inject('project') + ->callback($this->action(...)); + } + + public function action( + string $keyId, + array $scopes, + int $duration, + Response $response, + QueueEvent $queueForEvents, + Document $project, + ) { + $keyId = ID::unique(); + + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $duration, 0); + + $secret = $jwt->encode([ + 'projectId' => $project->getId(), + 'scopes' => $scopes + ]); + + $now = new \DateTime(); + $expire = $now->add(new \DateInterval('PT' . $duration . 'S'))->format('Y-m-d\TH:i:s.u\Z'); + + $key = new Document([ + '$id' => $keyId, + '$createdAt' => new DatabaseDateTime(), + '$updatedAt' => new DatabaseDateTime(), + 'name' => '', + 'scopes' => $scopes, + 'expire' => $expire, + 'sdks' => [], + 'accessedAt' => null, + 'secret' => API_KEY_DYNAMIC . '_' . $secret, + ]); + + $queueForEvents->setParam('keyId', $key->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($key, Response::MODEL_KEY); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Standard/Create.php similarity index 88% rename from src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Create.php rename to src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Standard/Create.php index 236c091c31..ccf19e4a30 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Standard/Create.php @@ -1,6 +1,6 @@ setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) - ->setHttpPath('/v1/project/keys') + ->setHttpPath('/v1/project/keys/standard') + ->httpAlias('/v1/project/keys') ->httpAlias('/v1/projects/:projectId/keys') - ->desc('Create project key') + ->desc('Create standard project key') ->groups(['api', 'project']) ->label('scope', 'keys.write') ->label('event', 'keys.[keyId].create') @@ -48,9 +49,11 @@ class Create extends Base ->label('sdk', new Method( namespace: 'project', group: 'keys', - name: 'createKey', + name: 'createStandardKey', description: <<assertEquals(201, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['jwt']); + $this->assertNotEmpty($response['body']['projectId']); + $this->assertSame($id, $response['body']['projectId']); $jwt = $response['body']['jwt']; From b2ce95a0cd6ec246067311537cbf2e4bf9437a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 Apr 2026 16:14:10 +0200 Subject: [PATCH 03/11] Dynamic key backwards compatibility --- app/controllers/api/projects.php | 2 -- app/controllers/general.php | 8 +++++ app/init/constants.php | 4 +-- app/init/models.php | 2 ++ src/Appwrite/Migration/Migration.php | 1 + .../Http/Project/Keys/Dynamic/Create.php | 32 +++++++---------- src/Appwrite/Utopia/Request/Filters/V24.php | 14 ++++++++ src/Appwrite/Utopia/Response.php | 1 + src/Appwrite/Utopia/Response/Filters/V24.php | 36 +++++++++++++++++++ .../Utopia/Response/Model/DynamicKey.php | 33 +++++++++++++++++ src/Appwrite/Utopia/Response/Model/Key.php | 5 --- 11 files changed, 109 insertions(+), 29 deletions(-) create mode 100644 src/Appwrite/Utopia/Request/Filters/V24.php create mode 100644 src/Appwrite/Utopia/Response/Filters/V24.php create mode 100644 src/Appwrite/Utopia/Response/Model/DynamicKey.php diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index ca7f8bb216..494aa11150 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1,6 +1,5 @@ addFilter(new RequestV23()); } + if (version_compare($requestFormat, '1.9.3', '<')) { + $request->addFilter(new RequestV24()); + } } $localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', '')); @@ -923,6 +928,9 @@ Http::init() */ $responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', '')); if ($responseFormat) { + if (version_compare($responseFormat, '1.9.3', '<')) { + $response->addFilter(new ResponseV24()); + } if (version_compare($responseFormat, '1.9.2', '<')) { $response->addFilter(new ResponseV23()); } diff --git a/app/init/constants.php b/app/init/constants.php index 8eacf2fe12..c3f67502f2 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -44,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 = 4323; -const APP_VERSION_STABLE = '1.9.2'; +const APP_CACHE_BUSTER = 4324; +const APP_VERSION_STABLE = '1.9.3'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; const APP_DATABASE_ATTRIBUTE_IP = 'ip'; diff --git a/app/init/models.php b/app/init/models.php index 77ca9be451..699d1561a3 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -70,6 +70,7 @@ use Appwrite\Utopia\Response\Model\DetectionRuntime; use Appwrite\Utopia\Response\Model\DetectionVariable; use Appwrite\Utopia\Response\Model\DevKey; use Appwrite\Utopia\Response\Model\Document as ModelDocument; +use Appwrite\Utopia\Response\Model\DynamicKey; use Appwrite\Utopia\Response\Model\Embedding; use Appwrite\Utopia\Response\Model\Error; use Appwrite\Utopia\Response\Model\ErrorDev; @@ -392,6 +393,7 @@ Response::setModel(new Execution()); Response::setModel(new Project()); Response::setModel(new Webhook()); Response::setModel(new Key()); +Response::setModel(new DynamicKey()); Response::setModel(new DevKey()); Response::setModel(new MockNumber()); Response::setModel(new OAuth2GitHub()); diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index ef0dd9f8b5..359925e368 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -95,6 +95,7 @@ abstract class Migration '1.9.0' => 'V24', '1.9.1' => 'V24', '1.9.2' => 'V24', + '1.9.3' => 'V24', ]; /** diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Dynamic/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Dynamic/Create.php index 2df1f2a303..eaad5a8c64 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Dynamic/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Dynamic/Create.php @@ -4,28 +4,20 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\Keys\Dynami; use Ahc\Jwt\JWT; use Appwrite\Event\Event as QueueEvent; -use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; -use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Response; use Utopia\Config\Config; -use Utopia\Database\Database; use Utopia\Database\DateTime as DatabaseDateTime; use Utopia\Database\Document; -use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Helpers\ID; -use Utopia\Database\Validator\Authorization; -use Utopia\Database\Validator\Datetime; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\ArrayList; -use Utopia\Validator\Nullable; use Utopia\Validator\Range; -use Utopia\Validator\Text; use Utopia\Validator\WhiteList; class Create extends Base @@ -62,7 +54,7 @@ class Create extends Base responses: [ new SDKResponse( code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_KEY, + model: Response::MODEL_DYNAMIC_KEY, ) ], )) @@ -84,16 +76,16 @@ class Create extends Base ) { $keyId = ID::unique(); - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $duration, 0); - - $secret = $jwt->encode([ - 'projectId' => $project->getId(), - 'scopes' => $scopes - ]); - - $now = new \DateTime(); - $expire = $now->add(new \DateInterval('PT' . $duration . 'S'))->format('Y-m-d\TH:i:s.u\Z'); - + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $duration, 0); + + $secret = $jwt->encode([ + 'projectId' => $project->getId(), + 'scopes' => $scopes + ]); + + $now = new \DateTime(); + $expire = $now->add(new \DateInterval('PT' . $duration . 'S'))->format('Y-m-d\TH:i:s.u\Z'); + $key = new Document([ '$id' => $keyId, '$createdAt' => new DatabaseDateTime(), @@ -110,6 +102,6 @@ class Create extends Base $response ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($key, Response::MODEL_KEY); + ->dynamic($key, Response::MODEL_DYNAMIC_KEY); } } diff --git a/src/Appwrite/Utopia/Request/Filters/V24.php b/src/Appwrite/Utopia/Request/Filters/V24.php new file mode 100644 index 0000000000..29df762f28 --- /dev/null +++ b/src/Appwrite/Utopia/Request/Filters/V24.php @@ -0,0 +1,14 @@ + $this->parseDynamicKey($content), + default => $content, + }; + } + + private function parseDynamicKey(array $content): array + { + unset($content['$id']); + unset($content['$createdAt']); + unset($content['$updatedAt']); + unset($content['name']); + unset($content['expire']); + unset($content['sdks']); + unset($content['accessedAt']); + + $content['jwt'] = $content['secret'] ?? ''; + unset($content['secret']); + + $content['projectId'] = 'WHAT DO I DO NOW?!'; + + return $content; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/DynamicKey.php b/src/Appwrite/Utopia/Response/Model/DynamicKey.php new file mode 100644 index 0000000000..c1016f3fcc --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/DynamicKey.php @@ -0,0 +1,33 @@ + Date: Tue, 28 Apr 2026 16:18:36 +0200 Subject: [PATCH 04/11] Bug&test fixing --- .../Modules/Project/Http/Project/Keys/Dynamic/Create.php | 7 +++---- src/Appwrite/Platform/Modules/Project/Services/Http.php | 6 ++++-- tests/e2e/Services/Projects/ProjectsConsoleClientTest.php | 1 + 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Dynamic/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Dynamic/Create.php index eaad5a8c64..8839a146fb 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Dynamic/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Dynamic/Create.php @@ -1,6 +1,6 @@ $keyId, - '$createdAt' => new DatabaseDateTime(), - '$updatedAt' => new DatabaseDateTime(), + '$createdAt' => DatabaseDateTime::now(), + '$updatedAt' => DatabaseDateTime::now(), 'name' => '', 'scopes' => $scopes, 'expire' => $expire, diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index 8c6b9da7e7..a0b2cd2acf 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -5,9 +5,10 @@ namespace Appwrite\Platform\Modules\Project\Services; use Appwrite\Platform\Modules\Project\Http\Init; use Appwrite\Platform\Modules\Project\Http\Project\AuthMethods\Update as UpdateAuthMethod; use Appwrite\Platform\Modules\Project\Http\Project\Delete as DeleteProject; -use Appwrite\Platform\Modules\Project\Http\Project\Keys\Create as CreateKey; use Appwrite\Platform\Modules\Project\Http\Project\Keys\Delete as DeleteKey; +use Appwrite\Platform\Modules\Project\Http\Project\Keys\Dynamic\Create as CreateDynamicKey; use Appwrite\Platform\Modules\Project\Http\Project\Keys\Get as GetKey; +use Appwrite\Platform\Modules\Project\Http\Project\Keys\Standard\Create as CreateStandardKey; use Appwrite\Platform\Modules\Project\Http\Project\Keys\Update as UpdateKey; use Appwrite\Platform\Modules\Project\Http\Project\Keys\XList as ListKeys; use Appwrite\Platform\Modules\Project\Http\Project\Labels\Update as UpdateProjectLabels; @@ -130,7 +131,8 @@ class Http extends Service $this->addAction(UpdateVariable::getName(), new UpdateVariable()); // Keys - $this->addAction(CreateKey::getName(), new CreateKey()); + $this->addAction(CreateStandardKey::getName(), new CreateStandardKey()); + $this->addAction(CreateDynamicKey::getName(), new CreateDynamicKey()); $this->addAction(ListKeys::getName(), new ListKeys()); $this->addAction(GetKey::getName(), new GetKey()); $this->addAction(DeleteKey::getName(), new DeleteKey()); diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 435b80ffd6..6936de9aff 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -3952,6 +3952,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/jwts', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.2', ], $this->getHeaders()), [ 'duration' => 5, 'scopes' => ['users.read'], From 11f80fc2edc8faf8b9ca33e2fb3a85414ae3093a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 Apr 2026 16:35:40 +0200 Subject: [PATCH 05/11] Solve key projectId backwards compatibility --- src/Appwrite/Utopia/Response/Filters/V24.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Filters/V24.php b/src/Appwrite/Utopia/Response/Filters/V24.php index 685e6d81bf..8ac5305dc1 100644 --- a/src/Appwrite/Utopia/Response/Filters/V24.php +++ b/src/Appwrite/Utopia/Response/Filters/V24.php @@ -26,11 +26,22 @@ class V24 extends Filter unset($content['sdks']); unset($content['accessedAt']); + $projectId = ''; + if (isset($content['secret'])) { + $parts = explode('_', $content['secret'], 2); + if (count($parts) === 2) { + $jwtParts = explode('.', $parts[1]); + if (count($jwtParts) >= 2) { + $payload = json_decode(base64_decode(str_replace(['-', '_'], ['+', '/'], $jwtParts[1])), true); + $projectId = $payload['projectId'] ?? ''; + } + } + } + $content['projectId'] = $projectId; + $content['jwt'] = $content['secret'] ?? ''; unset($content['secret']); - $content['projectId'] = 'WHAT DO I DO NOW?!'; - return $content; } } From 72dfd8a7bc2c474e3b76186550edeb872e028bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 Apr 2026 16:45:00 +0200 Subject: [PATCH 06/11] Add E2E tests for dynamic keys --- tests/e2e/Services/Project/KeysBase.php | 131 ++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/tests/e2e/Services/Project/KeysBase.php b/tests/e2e/Services/Project/KeysBase.php index 505c7f6539..5019c8fefd 100644 --- a/tests/e2e/Services/Project/KeysBase.php +++ b/tests/e2e/Services/Project/KeysBase.php @@ -239,6 +239,112 @@ trait KeysBase $this->deleteKey($customId); } + // ========================================================================= + // Create dynamic key tests + // ========================================================================= + + public function testCreateDynamicKey(): void + { + $key = $this->createDynamicKey( + ['users.read', 'users.write'], + ); + + $this->assertSame(201, $key['headers']['status-code']); + $this->assertNotEmpty($key['body']['$id']); + $this->assertSame('', $key['body']['name']); + $this->assertSame(['users.read', 'users.write'], $key['body']['scopes']); + $this->assertNotEmpty($key['body']['secret']); + $this->assertStringStartsWith(API_KEY_DYNAMIC . '_', $key['body']['secret']); + $this->assertSame([], $key['body']['sdks']); + $this->assertNull($key['body']['accessedAt']); + + $dateValidator = new DatetimeValidator(); + $this->assertSame(true, $dateValidator->isValid($key['body']['$createdAt'])); + $this->assertSame(true, $dateValidator->isValid($key['body']['$updatedAt'])); + $this->assertSame(true, $dateValidator->isValid($key['body']['expire'])); + + // Verify JWT payload + $jwt = substr($key['body']['secret'], strlen(API_KEY_DYNAMIC . '_')); + $parts = explode('.', $jwt); + $this->assertCount(3, $parts); + $payload = json_decode(base64_decode(str_replace(['-', '_'], ['+', '/'], $parts[1])), true); + $this->assertNotEmpty($payload['projectId']); + $this->assertSame(['users.read', 'users.write'], $payload['scopes']); + + // Verify default duration (900 seconds) + $expireDt = new \DateTime($key['body']['expire']); + $now = new \DateTime(); + $diff = $expireDt->getTimestamp() - $now->getTimestamp(); + $this->assertGreaterThanOrEqual(890, $diff); + $this->assertLessThanOrEqual(910, $diff); + } + + public function testCreateDynamicKeyWithDuration(): void + { + $duration = 1800; + + $key = $this->createDynamicKey( + ['databases.read'], + $duration, + ); + + $this->assertSame(201, $key['headers']['status-code']); + $this->assertSame(['databases.read'], $key['body']['scopes']); + + $expireDt = new \DateTime($key['body']['expire']); + $now = new \DateTime(); + $diff = $expireDt->getTimestamp() - $now->getTimestamp(); + $this->assertGreaterThanOrEqual($duration - 10, $diff); + $this->assertLessThanOrEqual($duration + 10, $diff); + } + + public function testCreateDynamicKeyWithEmptyScopes(): void + { + $key = $this->createDynamicKey( + [], + ); + + $this->assertSame(201, $key['headers']['status-code']); + $this->assertSame([], $key['body']['scopes']); + } + + public function testCreateDynamicKeyWithoutAuthentication(): void + { + $response = $this->createDynamicKey( + ['users.read'], + null, + false + ); + + $this->assertSame(401, $response['headers']['status-code']); + } + + public function testCreateDynamicKeyInvalidScope(): void + { + $response = $this->createDynamicKey( + ['invalid.scope'], + ); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testCreateDynamicKeyInvalidDuration(): void + { + $response = $this->createDynamicKey( + ['users.read'], + 0, + ); + + $this->assertSame(400, $response['headers']['status-code']); + + $response = $this->createDynamicKey( + ['users.read'], + 3601, + ); + + $this->assertSame(400, $response['headers']['status-code']); + } + // ========================================================================= // Update key tests // ========================================================================= @@ -855,4 +961,29 @@ trait KeysBase return $this->client->call(Client::METHOD_DELETE, '/project/keys/' . $keyId, $headers); } + + /** + * @param array $scopes + */ + protected function createDynamicKey(array $scopes, ?int $duration = null, bool $authenticated = true): mixed + { + $params = [ + 'scopes' => $scopes, + ]; + + if ($duration !== null) { + $params['duration'] = $duration; + } + + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]; + + if ($authenticated) { + $headers = array_merge($headers, $this->getHeaders()); + } + + return $this->client->call(Client::METHOD_POST, '/project/keys/dynamic', $headers, $params); + } } From f5a732d2311e9614e9496540924ace457a199b23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 Apr 2026 16:47:39 +0200 Subject: [PATCH 07/11] Add dynami key integration test --- .../Services/Project/KeysIntegrationTest.php | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 tests/e2e/Services/Project/KeysIntegrationTest.php diff --git a/tests/e2e/Services/Project/KeysIntegrationTest.php b/tests/e2e/Services/Project/KeysIntegrationTest.php new file mode 100644 index 0000000000..2615cac023 --- /dev/null +++ b/tests/e2e/Services/Project/KeysIntegrationTest.php @@ -0,0 +1,103 @@ +getProject()['$id']; + $apiKey = $this->getProject()['apiKey']; + + $serverHeaders = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $apiKey, + ]; + + $consoleHeaders = [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + 'x-appwrite-mode' => 'admin', + 'x-appwrite-project' => $projectId, + ]; + + // Step 1: Create a dynamic key scoped to users.read only. + $dynamicKey = $this->client->call( + Client::METHOD_POST, + '/project/keys/dynamic', + $serverHeaders, + [ + 'scopes' => ['users.read'], + 'duration' => 900, + ] + ); + $this->assertSame(201, $dynamicKey['headers']['status-code']); + $this->assertNotEmpty($dynamicKey['body']['secret']); + + $dynamicKeySecret = $dynamicKey['body']['secret']; + + $dynamicHeaders = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $dynamicKeySecret, + ]; + + // Step 2: Create a project user using console headers. + $user = $this->client->call( + Client::METHOD_POST, + '/users', + $consoleHeaders, + [ + 'userId' => ID::unique(), + 'email' => 'dynamic_key_' . \uniqid() . '@localhost.test', + 'password' => 'password1234', + 'name' => 'Dynamic Key Test User', + ] + ); + $this->assertSame(201, $user['headers']['status-code']); + $userId = $user['body']['$id']; + + // Step 3: Dynamic key can list users. + $list = $this->client->call( + Client::METHOD_GET, + '/users', + $dynamicHeaders + ); + $this->assertSame(200, $list['headers']['status-code']); + $this->assertGreaterThanOrEqual(1, $list['body']['total']); + + // Step 4: Dynamic key can get the specific user. + $get = $this->client->call( + Client::METHOD_GET, + '/users/' . $userId, + $dynamicHeaders + ); + $this->assertSame(200, $get['headers']['status-code']); + $this->assertSame($userId, $get['body']['$id']); + + // Step 5: Dynamic key cannot create users (missing users.write scope). + $createAttempt = $this->client->call( + Client::METHOD_POST, + '/users', + $dynamicHeaders, + [ + 'userId' => ID::unique(), + 'email' => 'should_fail_' . \uniqid() . '@localhost.test', + 'password' => 'password1234', + 'name' => 'Should Fail', + ] + ); + $this->assertSame(401, $createAttempt['headers']['status-code']); + } +} From 15917ac7ba69bc468079b57803d9d2e54aaa4d96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 Apr 2026 17:05:30 +0200 Subject: [PATCH 08/11] Fix failing tests --- src/Appwrite/Utopia/Request/Filters/V24.php | 15 +++++++++++++++ tests/e2e/Services/Project/KeysBase.php | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Utopia/Request/Filters/V24.php b/src/Appwrite/Utopia/Request/Filters/V24.php index 29df762f28..2809c6f2c6 100644 --- a/src/Appwrite/Utopia/Request/Filters/V24.php +++ b/src/Appwrite/Utopia/Request/Filters/V24.php @@ -9,6 +9,21 @@ class V24 extends Filter // Convert 1.9.2 params to 1.9.3 public function parse(array $content, string $model): array { + switch ($model) { + case 'project.createStandardKey': + $content = $this->parseKeyScopes($content); + break; + } + + return $content; + } + + protected function parseKeyScopes(array $content): array + { + if (!\is_array($content['scopes'] ?? null)) { + $content['scopes'] = []; + } + return $content; } } diff --git a/tests/e2e/Services/Project/KeysBase.php b/tests/e2e/Services/Project/KeysBase.php index 5019c8fefd..7ca494fefa 100644 --- a/tests/e2e/Services/Project/KeysBase.php +++ b/tests/e2e/Services/Project/KeysBase.php @@ -256,7 +256,7 @@ trait KeysBase $this->assertNotEmpty($key['body']['secret']); $this->assertStringStartsWith(API_KEY_DYNAMIC . '_', $key['body']['secret']); $this->assertSame([], $key['body']['sdks']); - $this->assertNull($key['body']['accessedAt']); + $this->assertSame('', $key['body']['accessedAt']); $dateValidator = new DatetimeValidator(); $this->assertSame(true, $dateValidator->isValid($key['body']['$createdAt'])); From c96836b1c0a1656d60cdfef4c0f0c4e70ab2f1d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 Apr 2026 17:10:58 +0200 Subject: [PATCH 09/11] Improve code quality of folder decoding project ID --- src/Appwrite/Utopia/Response/Filters/V24.php | 37 ++++++++++++-------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Filters/V24.php b/src/Appwrite/Utopia/Response/Filters/V24.php index 8ac5305dc1..29cc2ff4a1 100644 --- a/src/Appwrite/Utopia/Response/Filters/V24.php +++ b/src/Appwrite/Utopia/Response/Filters/V24.php @@ -2,8 +2,11 @@ namespace Appwrite\Utopia\Response\Filters; +use Ahc\Jwt\JWT; +use Ahc\Jwt\JWTException; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Filter; +use Utopia\System\System; // Convert 1.9.3 Data format to 1.9.2 format class V24 extends Filter @@ -26,22 +29,28 @@ class V24 extends Filter unset($content['sdks']); unset($content['accessedAt']); - $projectId = ''; - if (isset($content['secret'])) { - $parts = explode('_', $content['secret'], 2); - if (count($parts) === 2) { - $jwtParts = explode('.', $parts[1]); - if (count($jwtParts) >= 2) { - $payload = json_decode(base64_decode(str_replace(['-', '_'], ['+', '/'], $jwtParts[1])), true); - $projectId = $payload['projectId'] ?? ''; - } - } - } - $content['projectId'] = $projectId; - - $content['jwt'] = $content['secret'] ?? ''; + $secret = $content['secret'] ?? ''; unset($content['secret']); + $content['projectId'] = $this->extractProjectId($secret); + $content['jwt'] = $secret; + return $content; } + + private function extractProjectId(string $secret): string + { + $token = explode('_', $secret, 2)[1] ?? ''; + if ($token === '') { + return ''; + } + + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256'); + + try { + return $jwt->decode($token, false)['projectId'] ?? ''; + } catch (JWTException) { + return ''; + } + } } From 980762fc3ed7d2ccb907a8e8c6150debb6377b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 Apr 2026 17:18:06 +0200 Subject: [PATCH 10/11] Rename from dynamic key to ephemeral key (api keys) --- app/config/errors.php | 2 +- app/controllers/general.php | 2 +- app/controllers/shared/api.php | 3 +- app/init/constants.php | 2 +- app/init/models.php | 4 +-- src/Appwrite/Auth/Key.php | 8 +++-- .../Functions/Http/Executions/Create.php | 2 +- .../Modules/Functions/Workers/Builds.php | 4 +-- .../Modules/Functions/Workers/Screenshots.php | 2 +- .../Keys/{Dynamic => Ephemeral}/Create.php | 22 ++++++------ .../Http/Project/Keys/Standard/Create.php | 2 +- .../Modules/Project/Services/Http.php | 4 +-- src/Appwrite/Platform/Workers/Functions.php | 2 +- src/Appwrite/Platform/Workers/Migrations.php | 2 +- src/Appwrite/Utopia/Response.php | 2 +- src/Appwrite/Utopia/Response/Filters/V24.php | 4 +-- .../{DynamicKey.php => EphemeralKey.php} | 6 ++-- src/Appwrite/Vcs/Comment.php | 2 +- .../Functions/FunctionsCustomServerTest.php | 4 +-- tests/e2e/Services/Project/KeysBase.php | 36 +++++++++---------- .../Services/Project/KeysIntegrationTest.php | 34 +++++++++--------- .../Services/Sites/SitesCustomServerTest.php | 10 +++--- .../index.js | 0 .../package-lock.json | 4 +-- .../package.json | 2 +- .../setup.sh | 0 tests/unit/Auth/KeyTest.php | 24 ++++++------- 27 files changed, 96 insertions(+), 93 deletions(-) rename src/Appwrite/Platform/Modules/Project/Http/Project/Keys/{Dynamic => Ephemeral}/Create.php (81%) rename src/Appwrite/Utopia/Response/Model/{DynamicKey.php => EphemeralKey.php} (77%) rename tests/resources/functions/{dynamic-api-key => ephemeral-api-key}/index.js (100%) rename tests/resources/functions/{dynamic-api-key => ephemeral-api-key}/package-lock.json (93%) rename tests/resources/functions/{dynamic-api-key => ephemeral-api-key}/package.json (89%) rename tests/resources/functions/{dynamic-api-key => ephemeral-api-key}/setup.sh (100%) diff --git a/app/config/errors.php b/app/config/errors.php index 07b0cd59ed..fa112bcb6f 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -384,7 +384,7 @@ return [ ], Exception::API_KEY_EXPIRED => [ 'name' => Exception::API_KEY_EXPIRED, - 'description' => 'The dynamic API key has expired. Please don\'t use dynamic API keys for more than duration of the execution.', + 'description' => 'The ephemeral API key has expired. Please don\'t use ephemeral API keys for more than duration of the execution.', 'code' => 401, ], diff --git a/app/controllers/general.php b/app/controllers/general.php index 85d5cbedbd..eb4899a3d8 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -399,7 +399,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S 'projectId' => $project->getId(), 'scopes' => $resource->getAttribute('scopes', []) ]); - $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $jwtKey; + $headers['x-appwrite-key'] = API_KEY_EPHEMERAL . '_' . $jwtKey; $headers['x-appwrite-trigger'] = 'http'; $headers['x-appwrite-user-jwt'] = ''; diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 7c2f527ccf..c9e4f8b47d 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -183,7 +183,8 @@ Http::init() // Handle special app role case if ($apiKey->getRole() === User::ROLE_APPS) { // Disable authorization checks for project API keys - if (($apiKey->getType() === API_KEY_STANDARD || $apiKey->getType() === API_KEY_DYNAMIC) && $apiKey->getProjectId() === $project->getId()) { + // Dynamic supported for backwards compatibility + if (($apiKey->getType() === API_KEY_STANDARD || $apiKey->getType() === API_KEY_EPHEMERAL || $apiKey->getType() === 'dynamic') && $apiKey->getProjectId() === $project->getId()) { $authorization->setDefaultStatus(false); } diff --git a/app/init/constants.php b/app/init/constants.php index c3f67502f2..a4cef6f035 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -255,7 +255,7 @@ const MESSAGE_TYPE_SMS = 'sms'; const MESSAGE_TYPE_PUSH = 'push'; // API key types const API_KEY_STANDARD = 'standard'; -const API_KEY_DYNAMIC = 'dynamic'; +const API_KEY_EPHEMERAL = 'ephemeral'; const API_KEY_ORGANIZATION = 'organization'; const API_KEY_ACCOUNT = 'account'; // Usage metrics diff --git a/app/init/models.php b/app/init/models.php index 699d1561a3..56f24ddc2c 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -70,8 +70,8 @@ use Appwrite\Utopia\Response\Model\DetectionRuntime; use Appwrite\Utopia\Response\Model\DetectionVariable; use Appwrite\Utopia\Response\Model\DevKey; use Appwrite\Utopia\Response\Model\Document as ModelDocument; -use Appwrite\Utopia\Response\Model\DynamicKey; use Appwrite\Utopia\Response\Model\Embedding; +use Appwrite\Utopia\Response\Model\EphemeralKey; use Appwrite\Utopia\Response\Model\Error; use Appwrite\Utopia\Response\Model\ErrorDev; use Appwrite\Utopia\Response\Model\Execution; @@ -393,7 +393,7 @@ Response::setModel(new Execution()); Response::setModel(new Project()); Response::setModel(new Webhook()); Response::setModel(new Key()); -Response::setModel(new DynamicKey()); +Response::setModel(new EphemeralKey()); Response::setModel(new DevKey()); Response::setModel(new MockNumber()); Response::setModel(new OAuth2GitHub()); diff --git a/src/Appwrite/Auth/Key.php b/src/Appwrite/Auth/Key.php index 8f645f6f08..0cbaefa4b3 100644 --- a/src/Appwrite/Auth/Key.php +++ b/src/Appwrite/Auth/Key.php @@ -105,7 +105,7 @@ class Key /** * Decode the given secret key into a Key object, containing the project ID, type, role, scopes, and name. - * Can be a stored API key or a dynamic key (JWT). + * Can be a stored API key or an ephemeral key (JWT). * * @throws Exception */ @@ -138,7 +138,9 @@ class Key ); switch ($type) { - case API_KEY_DYNAMIC: + // Dynamic supported for backwards compatibility + case API_KEY_EPHEMERAL: + case 'dynamic': $jwtObj = new JWT( key: System::getEnv('_APP_OPENSSL_KEY_V1'), algo: 'HS256', @@ -153,7 +155,7 @@ class Key $expired = true; } - $name = $payload['name'] ?? 'Dynamic Key'; + $name = $payload['name'] ?? 'Ephemeral Key'; $projectId = $payload['projectId'] ?? ''; $disabledMetrics = $payload['disabledMetrics'] ?? []; $hostnameOverride = $payload['hostnameOverride'] ?? false; diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index 5b2f4ff297..4bf2fbc48f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -228,7 +228,7 @@ class Create extends Base $executionId = ID::unique(); $headers['x-appwrite-execution-id'] = $executionId; - $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey; + $headers['x-appwrite-key'] = API_KEY_EPHEMERAL . '_' . $apiKey; $headers['x-appwrite-trigger'] = 'http'; $headers['x-appwrite-user-id'] = $user->getId(); $headers['x-appwrite-user-jwt'] = $jwt; diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 286f1c55ee..352fb56e28 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -624,7 +624,7 @@ class Builds extends Action $vars = [ ...$vars, 'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint, - 'APPWRITE_FUNCTION_API_KEY' => API_KEY_DYNAMIC . '_' . $apiKey, + 'APPWRITE_FUNCTION_API_KEY' => API_KEY_EPHEMERAL . '_' . $apiKey, 'APPWRITE_FUNCTION_ID' => $resource->getId(), 'APPWRITE_FUNCTION_NAME' => $resource->getAttribute('name'), 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), @@ -639,7 +639,7 @@ class Builds extends Action $vars = [ ...$vars, 'APPWRITE_SITE_API_ENDPOINT' => $endpoint, - 'APPWRITE_SITE_API_KEY' => API_KEY_DYNAMIC . '_' . $apiKey, + 'APPWRITE_SITE_API_KEY' => API_KEY_EPHEMERAL . '_' . $apiKey, 'APPWRITE_SITE_ID' => $resource->getId(), 'APPWRITE_SITE_NAME' => $resource->getAttribute('name'), 'APPWRITE_SITE_DEPLOYMENT' => $deployment->getId(), diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php index a6f1ca1b03..7d1cdc4980 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php @@ -168,7 +168,7 @@ class Screenshots extends Action $config = $configs[$key]; $config['headers'] = \array_merge($config['headers'], [ - 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey + 'x-appwrite-key' => API_KEY_EPHEMERAL . '_' . $apiKey ]); $config['sleep'] = 3000; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Dynamic/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php similarity index 81% rename from src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Dynamic/Create.php rename to src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php index 8839a146fb..cf21eaec74 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Dynamic/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php @@ -1,6 +1,6 @@ setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) - ->setHttpPath('/v1/project/keys/dynamic') + ->setHttpPath('/v1/project/keys/ephemeral') ->httpAlias('/v1/projects/:projectId/jwts') - ->desc('Create dynamic project key') + ->desc('Create ephemeral project key') ->groups(['api', 'project']) ->label('scope', 'keys.write') ->label('event', 'keys.[keyId].create') @@ -44,22 +44,22 @@ class Create extends Base ->label('sdk', new Method( namespace: 'project', group: 'keys', - name: 'createDynamicKey', + name: 'createEphemeralKey', description: <<param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', optional: false) - ->param('duration', 900, new Range(1, 3600), 'Time in seconds before dynamic key expires. Default duration is 900 seconds, and maximum is 3600 seconds.', true) + ->param('duration', 900, new Range(1, 3600), 'Time in seconds before ephemeral key expires. Default duration is 900 seconds, and maximum is 3600 seconds.', true) ->inject('response') ->inject('queueForEvents') ->inject('project') @@ -94,13 +94,13 @@ class Create extends Base 'expire' => $expire, 'sdks' => [], 'accessedAt' => null, - 'secret' => API_KEY_DYNAMIC . '_' . $secret, + 'secret' => API_KEY_EPHEMERAL . '_' . $secret, ]); $queueForEvents->setParam('keyId', $key->getId()); $response ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($key, Response::MODEL_DYNAMIC_KEY); + ->dynamic($key, Response::MODEL_EPHEMERAL_KEY); } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Standard/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Standard/Create.php index ccf19e4a30..67bdcc09a6 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Standard/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Standard/Create.php @@ -53,7 +53,7 @@ class Create extends Base description: <<addAction(CreateStandardKey::getName(), new CreateStandardKey()); - $this->addAction(CreateDynamicKey::getName(), new CreateDynamicKey()); + $this->addAction(CreateEphemeralKey::getName(), new CreateEphemeralKey()); $this->addAction(ListKeys::getName(), new ListKeys()); $this->addAction(GetKey::getName(), new GetKey()); $this->addAction(DeleteKey::getName(), new DeleteKey()); diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index 28c298b050..8167fb975d 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -434,7 +434,7 @@ class Functions extends Action ]); $headers['x-appwrite-execution-id'] = $executionId ?? ''; - $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey; + $headers['x-appwrite-key'] = API_KEY_EPHEMERAL . '_' . $apiKey; $headers['x-appwrite-trigger'] = $trigger; $headers['x-appwrite-event'] = $event ?? ''; $headers['x-appwrite-user-id'] = $user->getId(); diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index fa2ed5883f..69f72b8e27 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -402,7 +402,7 @@ class Migrations extends Action ] ]); - return API_KEY_DYNAMIC . '_' . $apiKey; + return API_KEY_EPHEMERAL . '_' . $apiKey; } /** diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 92eb0768b3..b6c0fcc1ab 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -251,7 +251,7 @@ class Response extends SwooleResponse public const MODEL_WEBHOOK_LIST = 'webhookList'; public const MODEL_KEY = 'key'; public const MODEL_KEY_LIST = 'keyList'; - public const MODEL_DYNAMIC_KEY = 'dynamicKey'; + public const MODEL_EPHEMERAL_KEY = 'ephemeralKey'; public const MODEL_DEV_KEY = 'devKey'; public const MODEL_DEV_KEY_LIST = 'devKeyList'; public const MODEL_MOCK_NUMBER = 'mockNumber'; diff --git a/src/Appwrite/Utopia/Response/Filters/V24.php b/src/Appwrite/Utopia/Response/Filters/V24.php index 29cc2ff4a1..46db062863 100644 --- a/src/Appwrite/Utopia/Response/Filters/V24.php +++ b/src/Appwrite/Utopia/Response/Filters/V24.php @@ -14,12 +14,12 @@ class V24 extends Filter public function parse(array $content, string $model): array { return match ($model) { - Response::MODEL_DYNAMIC_KEY => $this->parseDynamicKey($content), + Response::MODEL_EPHEMERAL_KEY => $this->parseEphemeralKey($content), default => $content, }; } - private function parseDynamicKey(array $content): array + private function parseEphemeralKey(array $content): array { unset($content['$id']); unset($content['$createdAt']); diff --git a/src/Appwrite/Utopia/Response/Model/DynamicKey.php b/src/Appwrite/Utopia/Response/Model/EphemeralKey.php similarity index 77% rename from src/Appwrite/Utopia/Response/Model/DynamicKey.php rename to src/Appwrite/Utopia/Response/Model/EphemeralKey.php index c1016f3fcc..f6b7fdd7f3 100644 --- a/src/Appwrite/Utopia/Response/Model/DynamicKey.php +++ b/src/Appwrite/Utopia/Response/Model/EphemeralKey.php @@ -4,7 +4,7 @@ namespace Appwrite\Utopia\Response\Model; use Appwrite\Utopia\Response; -class DynamicKey extends Key +class EphemeralKey extends Key { public function __construct() { @@ -18,7 +18,7 @@ class DynamicKey extends Key */ public function getName(): string { - return 'Dynamic Key'; + return 'Ephemeral Key'; } /** @@ -28,6 +28,6 @@ class DynamicKey extends Key */ public function getType(): string { - return Response::MODEL_DYNAMIC_KEY; + return Response::MODEL_EPHEMERAL_KEY; } } diff --git a/src/Appwrite/Vcs/Comment.php b/src/Appwrite/Vcs/Comment.php index 6214bb1f29..4dc0174e50 100644 --- a/src/Appwrite/Vcs/Comment.php +++ b/src/Appwrite/Vcs/Comment.php @@ -31,7 +31,7 @@ class Comment 'Trigger functions via HTTP, SDKs, events, webhooks, or scheduled cron jobs', 'Each function runs in its own isolated container with custom environment variables', 'Build commands execute in runtime containers during deployment', - 'Dynamic API keys are generated automatically for each function execution', + 'Ephemeral API keys are generated automatically for each function execution', 'JWT tokens let functions act on behalf of users while preserving their permissions', 'Storage files get ClamAV malware scanning and encryption by default', 'Roll back Sites deployments instantly by switching between versions', diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 4255774f18..e75c3e5f4e 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -700,7 +700,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(200, $function['headers']['status-code']); $this->assertEquals($deploymentId, $function['body']['deploymentId']); - // Test starter code is used and that dynamic keys work + // Test starter code is used and that ephemeral keys work $execution = $this->createExecution($functionId, [ 'path' => '/ping', ]); @@ -2129,7 +2129,7 @@ class FunctionsCustomServerTest extends Scope ]); $deploymentId = $this->setupDeployment($functionId, [ - 'code' => $this->packageFunction('dynamic-api-key'), + 'code' => $this->packageFunction('ephemeral-api-key'), 'activate' => true, ]); diff --git a/tests/e2e/Services/Project/KeysBase.php b/tests/e2e/Services/Project/KeysBase.php index 7ca494fefa..cd50f67c14 100644 --- a/tests/e2e/Services/Project/KeysBase.php +++ b/tests/e2e/Services/Project/KeysBase.php @@ -240,12 +240,12 @@ trait KeysBase } // ========================================================================= - // Create dynamic key tests + // Create ephemeral key tests // ========================================================================= - public function testCreateDynamicKey(): void + public function testCreateEphemeralKey(): void { - $key = $this->createDynamicKey( + $key = $this->createEphemeralKey( ['users.read', 'users.write'], ); @@ -254,7 +254,7 @@ trait KeysBase $this->assertSame('', $key['body']['name']); $this->assertSame(['users.read', 'users.write'], $key['body']['scopes']); $this->assertNotEmpty($key['body']['secret']); - $this->assertStringStartsWith(API_KEY_DYNAMIC . '_', $key['body']['secret']); + $this->assertStringStartsWith(API_KEY_EPHEMERAL . '_', $key['body']['secret']); $this->assertSame([], $key['body']['sdks']); $this->assertSame('', $key['body']['accessedAt']); @@ -264,7 +264,7 @@ trait KeysBase $this->assertSame(true, $dateValidator->isValid($key['body']['expire'])); // Verify JWT payload - $jwt = substr($key['body']['secret'], strlen(API_KEY_DYNAMIC . '_')); + $jwt = substr($key['body']['secret'], strlen(API_KEY_EPHEMERAL . '_')); $parts = explode('.', $jwt); $this->assertCount(3, $parts); $payload = json_decode(base64_decode(str_replace(['-', '_'], ['+', '/'], $parts[1])), true); @@ -279,11 +279,11 @@ trait KeysBase $this->assertLessThanOrEqual(910, $diff); } - public function testCreateDynamicKeyWithDuration(): void + public function testCreateEphemeralKeyWithDuration(): void { $duration = 1800; - $key = $this->createDynamicKey( + $key = $this->createEphemeralKey( ['databases.read'], $duration, ); @@ -298,9 +298,9 @@ trait KeysBase $this->assertLessThanOrEqual($duration + 10, $diff); } - public function testCreateDynamicKeyWithEmptyScopes(): void + public function testCreateEphemeralKeyWithEmptyScopes(): void { - $key = $this->createDynamicKey( + $key = $this->createEphemeralKey( [], ); @@ -308,9 +308,9 @@ trait KeysBase $this->assertSame([], $key['body']['scopes']); } - public function testCreateDynamicKeyWithoutAuthentication(): void + public function testCreateEphemeralKeyWithoutAuthentication(): void { - $response = $this->createDynamicKey( + $response = $this->createEphemeralKey( ['users.read'], null, false @@ -319,25 +319,25 @@ trait KeysBase $this->assertSame(401, $response['headers']['status-code']); } - public function testCreateDynamicKeyInvalidScope(): void + public function testCreateEphemeralKeyInvalidScope(): void { - $response = $this->createDynamicKey( + $response = $this->createEphemeralKey( ['invalid.scope'], ); $this->assertSame(400, $response['headers']['status-code']); } - public function testCreateDynamicKeyInvalidDuration(): void + public function testCreateEphemeralKeyInvalidDuration(): void { - $response = $this->createDynamicKey( + $response = $this->createEphemeralKey( ['users.read'], 0, ); $this->assertSame(400, $response['headers']['status-code']); - $response = $this->createDynamicKey( + $response = $this->createEphemeralKey( ['users.read'], 3601, ); @@ -965,7 +965,7 @@ trait KeysBase /** * @param array $scopes */ - protected function createDynamicKey(array $scopes, ?int $duration = null, bool $authenticated = true): mixed + protected function createEphemeralKey(array $scopes, ?int $duration = null, bool $authenticated = true): mixed { $params = [ 'scopes' => $scopes, @@ -984,6 +984,6 @@ trait KeysBase $headers = array_merge($headers, $this->getHeaders()); } - return $this->client->call(Client::METHOD_POST, '/project/keys/dynamic', $headers, $params); + return $this->client->call(Client::METHOD_POST, '/project/keys/ephemeral', $headers, $params); } } diff --git a/tests/e2e/Services/Project/KeysIntegrationTest.php b/tests/e2e/Services/Project/KeysIntegrationTest.php index 2615cac023..4dc5838e72 100644 --- a/tests/e2e/Services/Project/KeysIntegrationTest.php +++ b/tests/e2e/Services/Project/KeysIntegrationTest.php @@ -13,7 +13,7 @@ class KeysIntegrationTest extends Scope use ProjectCustom; use SideServer; - public function testDynamicKeyScopeEnforcement(): void + public function testEphemeralKeyScopeEnforcement(): void { $projectId = $this->getProject()['$id']; $apiKey = $this->getProject()['apiKey']; @@ -32,25 +32,25 @@ class KeysIntegrationTest extends Scope 'x-appwrite-project' => $projectId, ]; - // Step 1: Create a dynamic key scoped to users.read only. - $dynamicKey = $this->client->call( + // Step 1: Create an ephemeral key scoped to users.read only. + $ephemeralKey = $this->client->call( Client::METHOD_POST, - '/project/keys/dynamic', + '/project/keys/ephemeral', $serverHeaders, [ 'scopes' => ['users.read'], 'duration' => 900, ] ); - $this->assertSame(201, $dynamicKey['headers']['status-code']); - $this->assertNotEmpty($dynamicKey['body']['secret']); + $this->assertSame(201, $ephemeralKey['headers']['status-code']); + $this->assertNotEmpty($ephemeralKey['body']['secret']); - $dynamicKeySecret = $dynamicKey['body']['secret']; + $ephemeralKeySecret = $ephemeralKey['body']['secret']; - $dynamicHeaders = [ + $ephemeralHeaders = [ 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $dynamicKeySecret, + 'x-appwrite-key' => $ephemeralKeySecret, ]; // Step 2: Create a project user using console headers. @@ -60,37 +60,37 @@ class KeysIntegrationTest extends Scope $consoleHeaders, [ 'userId' => ID::unique(), - 'email' => 'dynamic_key_' . \uniqid() . '@localhost.test', + 'email' => 'ephemeral_key_' . \uniqid() . '@localhost.test', 'password' => 'password1234', - 'name' => 'Dynamic Key Test User', + 'name' => 'Ephemeral Key Test User', ] ); $this->assertSame(201, $user['headers']['status-code']); $userId = $user['body']['$id']; - // Step 3: Dynamic key can list users. + // Step 3: Ephemeral key can list users. $list = $this->client->call( Client::METHOD_GET, '/users', - $dynamicHeaders + $ephemeralHeaders ); $this->assertSame(200, $list['headers']['status-code']); $this->assertGreaterThanOrEqual(1, $list['body']['total']); - // Step 4: Dynamic key can get the specific user. + // Step 4: Ephemeral key can get the specific user. $get = $this->client->call( Client::METHOD_GET, '/users/' . $userId, - $dynamicHeaders + $ephemeralHeaders ); $this->assertSame(200, $get['headers']['status-code']); $this->assertSame($userId, $get['body']['$id']); - // Step 5: Dynamic key cannot create users (missing users.write scope). + // Step 5: Ephemeral key cannot create users (missing users.write scope). $createAttempt = $this->client->call( Client::METHOD_POST, '/users', - $dynamicHeaders, + $ephemeralHeaders, [ 'userId' => ID::unique(), 'email' => 'should_fail_' . \uniqid() . '@localhost.test', diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 71f6675561..42fd190172 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -2038,7 +2038,7 @@ class SitesCustomServerTest extends Scope 'previewAuthDisabled' => true, ]); $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false, headers: [ - 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey, + 'x-appwrite-key' => API_KEY_EPHEMERAL . '_' . $apiKey, ]); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Hello Appwrite", $response['body']); @@ -2046,7 +2046,7 @@ class SitesCustomServerTest extends Scope $this->assertGreaterThan($contentLength, $response['headers']['content-length']); $response = $proxyClient->call(Client::METHOD_GET, '/non-existing-path', followRedirects: false, headers: [ - 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey, + 'x-appwrite-key' => API_KEY_EPHEMERAL . '_' . $apiKey, ]); $this->assertEquals(404, $response['headers']['status-code']); $this->assertStringContainsString("Page not found", $response['body']); @@ -2882,7 +2882,7 @@ class SitesCustomServerTest extends Scope ]); $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false, headers: [ - 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey, + 'x-appwrite-key' => API_KEY_EPHEMERAL . '_' . $apiKey, ]); $this->assertEquals(400, $response['headers']['status-code']); $deployment = $this->getDeployment($siteId, $deploymentId); @@ -2924,7 +2924,7 @@ class SitesCustomServerTest extends Scope // deployment is still building error page $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false, headers: [ - 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey, + 'x-appwrite-key' => API_KEY_EPHEMERAL . '_' . $apiKey, ]); $this->assertEquals(400, $response['headers']['status-code']); $this->assertStringContainsString("Deployment is still building", $response['body']); @@ -2939,7 +2939,7 @@ class SitesCustomServerTest extends Scope // deployment failed error page $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false, headers: [ - 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey, + 'x-appwrite-key' => API_KEY_EPHEMERAL . '_' . $apiKey, ]); $this->assertEquals(400, $response['headers']['status-code']); $this->assertStringContainsString("Deployment build failed", $response['body']); diff --git a/tests/resources/functions/dynamic-api-key/index.js b/tests/resources/functions/ephemeral-api-key/index.js similarity index 100% rename from tests/resources/functions/dynamic-api-key/index.js rename to tests/resources/functions/ephemeral-api-key/index.js diff --git a/tests/resources/functions/dynamic-api-key/package-lock.json b/tests/resources/functions/ephemeral-api-key/package-lock.json similarity index 93% rename from tests/resources/functions/dynamic-api-key/package-lock.json rename to tests/resources/functions/ephemeral-api-key/package-lock.json index 2d86fe18d3..3756c13c0c 100644 --- a/tests/resources/functions/dynamic-api-key/package-lock.json +++ b/tests/resources/functions/ephemeral-api-key/package-lock.json @@ -1,11 +1,11 @@ { - "name": "dynamic-api-key", + "name": "ephemeral-api-key", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "dynamic-api-key", + "name": "ephemeral-api-key", "version": "1.0.0", "license": "ISC", "dependencies": { diff --git a/tests/resources/functions/dynamic-api-key/package.json b/tests/resources/functions/ephemeral-api-key/package.json similarity index 89% rename from tests/resources/functions/dynamic-api-key/package.json rename to tests/resources/functions/ephemeral-api-key/package.json index 19b8158131..35abec4874 100644 --- a/tests/resources/functions/dynamic-api-key/package.json +++ b/tests/resources/functions/ephemeral-api-key/package.json @@ -1,5 +1,5 @@ { - "name": "dynamic-api-key", + "name": "ephemeral-api-key", "version": "1.0.0", "main": "index.js", "scripts": { diff --git a/tests/resources/functions/dynamic-api-key/setup.sh b/tests/resources/functions/ephemeral-api-key/setup.sh similarity index 100% rename from tests/resources/functions/dynamic-api-key/setup.sh rename to tests/resources/functions/ephemeral-api-key/setup.sh diff --git a/tests/unit/Auth/KeyTest.php b/tests/unit/Auth/KeyTest.php index 58fe3113e1..bcdb46180f 100644 --- a/tests/unit/Auth/KeyTest.php +++ b/tests/unit/Auth/KeyTest.php @@ -14,7 +14,7 @@ class KeyTest extends TestCase { public function testDecode(): void { - // Decode dynamic key + // Decode ephemeral key $projectId = 'test'; $usage = false; $scopes = [ @@ -36,12 +36,12 @@ class KeyTest extends TestCase $this->assertEquals($projectId, $decoded->getProjectId()); $this->assertEquals('', $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); - $this->assertEquals(API_KEY_DYNAMIC, $decoded->getType()); + $this->assertEquals(API_KEY_EPHEMERAL, $decoded->getType()); $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); $this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes()); - $this->assertEquals('Dynamic Key', $decoded->getName()); + $this->assertEquals('Ephemeral Key', $decoded->getName()); - // Decode dynamic key with extras + // Decode ephemeral key with extras $extra = [ 'disabledMetrics' => ['metric123'], 'hostnameOverride' => true, @@ -60,10 +60,10 @@ class KeyTest extends TestCase $this->assertEquals($projectId, $decoded->getProjectId()); $this->assertEquals('', $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); - $this->assertEquals(API_KEY_DYNAMIC, $decoded->getType()); + $this->assertEquals(API_KEY_EPHEMERAL, $decoded->getType()); $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); $this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes()); - $this->assertEquals('Dynamic Key', $decoded->getName()); + $this->assertEquals('Ephemeral Key', $decoded->getName()); $this->assertEquals(['metric123'], $decoded->getDisabledMetrics()); $this->assertEquals(true, $decoded->getHostnameOverride()); $this->assertEquals(true, $decoded->isBannerDisabled()); @@ -71,8 +71,8 @@ class KeyTest extends TestCase $this->assertEquals(true, $decoded->isPreviewAuthDisabled()); $this->assertEquals(true, $decoded->isDeploymentStatusIgnored()); - // Decode invalid dynamic key - $invalidKey = API_KEY_DYNAMIC . '_invalid_jwt_token'; + // Decode invalid ephemeral key + $invalidKey = API_KEY_EPHEMERAL . '_invalid_jwt_token'; $decoded = Key::decode( project: new Document(['$id' => $projectId]), team: new Document(), @@ -82,12 +82,12 @@ class KeyTest extends TestCase $this->assertEquals($projectId, $decoded->getProjectId()); $this->assertEquals('', $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); - $this->assertEquals(API_KEY_DYNAMIC, $decoded->getType()); + $this->assertEquals(API_KEY_EPHEMERAL, $decoded->getType()); $this->assertEquals(User::ROLE_GUESTS, $decoded->getRole()); $this->assertEquals($guestRoleScopes, $decoded->getScopes()); $this->assertEquals('UNKNOWN', $decoded->getName()); - // Decode expired dynamic key + // Decode expired ephemeral key $expiredKey = self::generateKey($projectId, $usage, $scopes, maxAge: 1, timestamp: time() - 60); \sleep(2); $decoded = Key::decode( @@ -99,7 +99,7 @@ class KeyTest extends TestCase $this->assertEquals($projectId, $decoded->getProjectId()); $this->assertEquals('', $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); - $this->assertEquals(API_KEY_DYNAMIC, $decoded->getType()); + $this->assertEquals(API_KEY_EPHEMERAL, $decoded->getType()); $this->assertEquals(User::ROLE_GUESTS, $decoded->getRole()); $this->assertEquals($guestRoleScopes, $decoded->getScopes()); $this->assertEquals('UNKNOWN', $decoded->getName()); @@ -363,6 +363,6 @@ class KeyTest extends TestCase 'scopes' => $scopes, ], $extra)); - return API_KEY_DYNAMIC . '_' . $apiKey; + return API_KEY_EPHEMERAL . '_' . $apiKey; } } From 05f2d2b9cf87aadcc249202ddb02d9e4a8ae6639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 Apr 2026 19:29:37 +0200 Subject: [PATCH 11/11] Fix tests --- src/Appwrite/Utopia/Request/Filters/V24.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Appwrite/Utopia/Request/Filters/V24.php b/src/Appwrite/Utopia/Request/Filters/V24.php index 2809c6f2c6..f62c1f8c0b 100644 --- a/src/Appwrite/Utopia/Request/Filters/V24.php +++ b/src/Appwrite/Utopia/Request/Filters/V24.php @@ -11,6 +11,7 @@ class V24 extends Filter { switch ($model) { case 'project.createStandardKey': + $content = $this->fillKeyId($content); $content = $this->parseKeyScopes($content); break; } @@ -18,6 +19,12 @@ class V24 extends Filter return $content; } + protected function fillKeyId(array $content): array + { + $content['keyId'] = $content['keyId'] ?? 'unique()'; + return $content; + } + protected function parseKeyScopes(array $content): array { if (!\is_array($content['scopes'] ?? null)) {