Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Change Log

## 30.3.1

* Fixed: added `ProjectAuthMethodId::PASSKEY()`, so `project.updateAuthMethod` can enable passkeys
* Fixed: added `ProjectPolicyId::PASSKEY()` and the `PolicyPasskey` model, so `getPolicy` and `listPolicies` return the passkey policy

## 30.3.0

* Added: `account` passkey methods `listPasskeys`, `createPasskey`, `getPasskey`, `updatePasskey`, `deletePasskey` and `updatePasskeyVerification`
Expand Down
4 changes: 2 additions & 2 deletions src/Appwrite/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ class Client
*
*/
protected array $headers = [
'user-agent' => 'AppwritePHPSDK/30.3.0 ()',
'user-agent' => 'AppwritePHPSDK/30.3.1 ()',
'x-sdk-name' => 'PHP',
'x-sdk-platform' => 'server',
'x-sdk-language' => 'php',
'x-sdk-version' => '30.3.0',
'x-sdk-version' => '30.3.1',
];

/**
Expand Down
8 changes: 8 additions & 0 deletions src/Appwrite/Enums/ProjectAuthMethodId.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class ProjectAuthMethodId implements JsonSerializable, Stringable
private static ProjectAuthMethodId $INVITES;
private static ProjectAuthMethodId $JWT;
private static ProjectAuthMethodId $PHONE;
private static ProjectAuthMethodId $PASSKEY;

private function __construct(private readonly string $value)
{
Expand Down Expand Up @@ -73,6 +74,12 @@ public static function PHONE(): ProjectAuthMethodId

return self::$PHONE;
}
public static function PASSKEY(): ProjectAuthMethodId
{
self::$PASSKEY ??= new ProjectAuthMethodId('passkey');

return self::$PASSKEY;
}

public static function from(string $value): self
{
Expand All @@ -84,6 +91,7 @@ public static function from(string $value): self
'invites' => self::INVITES(),
'jwt' => self::JWT(),
'phone' => self::PHONE(),
'passkey' => self::PASSKEY(),
default => throw new \InvalidArgumentException('Unknown ProjectAuthMethodId value: ' . $value),
};
}
Expand Down
8 changes: 8 additions & 0 deletions src/Appwrite/Enums/ProjectPolicyId.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class ProjectPolicyId implements JsonSerializable, Stringable
private static ProjectPolicyId $DENYDISPOSABLEEMAIL;
private static ProjectPolicyId $DENYFREEEMAIL;
private static ProjectPolicyId $DENYCORPORATEEMAIL;
private static ProjectPolicyId $PASSKEY;

private function __construct(private readonly string $value)
{
Expand Down Expand Up @@ -129,6 +130,12 @@ public static function DENYCORPORATEEMAIL(): ProjectPolicyId

return self::$DENYCORPORATEEMAIL;
}
public static function PASSKEY(): ProjectPolicyId
{
self::$PASSKEY ??= new ProjectPolicyId('passkey');

return self::$PASSKEY;
}

public static function from(string $value): self
{
Expand All @@ -148,6 +155,7 @@ public static function from(string $value): self
'deny-disposable-email' => self::DENYDISPOSABLEEMAIL(),
'deny-free-email' => self::DENYFREEEMAIL(),
'deny-corporate-email' => self::DENYCORPORATEEMAIL(),
'passkey' => self::PASSKEY(),
default => throw new \InvalidArgumentException('Unknown ProjectPolicyId value: ' . $value),
};
}
Expand Down
63 changes: 63 additions & 0 deletions src/Appwrite/Models/PolicyPasskey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace Appwrite\Models;

/**
* Policy Passkey
*
* @phpstan-consistent-constructor
*/
readonly class PolicyPasskey
{
use ArraySerializable;

/**
* PolicyPasskey constructor.
*
* @param string $id policy id.
* @param string $rpId relying party id passkeys are bound to. empty until configured.
* @param array $origins web origins passkeys work on, from the project's platforms on the relying party id and a localhost web platform. read-only.
*/
public function __construct(
public string $id,
public string $rpId,
public array $origins
) {
}

/**
* @param array<string, mixed> $data
*/
public static function from(array $data): static
{
if (!array_key_exists('$id', $data)) {
throw new \InvalidArgumentException('Missing required field "$id" for ' . static::class . '.');
}
if (!array_key_exists('rpId', $data)) {
throw new \InvalidArgumentException('Missing required field "rpId" for ' . static::class . '.');
}
if (!array_key_exists('origins', $data)) {
throw new \InvalidArgumentException('Missing required field "origins" for ' . static::class . '.');
}

return new static(
id: $data['$id'],
rpId: $data['rpId'],
origins: $data['origins']
);
}

/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'$id' => static::serializeValue($this->id),
'rpId' => static::serializeValue($this->rpId),
'origins' => static::serializeValue($this->origins)
];
}
}
6 changes: 5 additions & 1 deletion src/Appwrite/Services/Project.php
Original file line number Diff line number Diff line change
Expand Up @@ -3764,7 +3764,7 @@ public function updateUserLimitPolicy(?int $total): \Appwrite\Models\Project
*
* @throws AppwriteException
*/
public function getPolicy(ProjectPolicyId $policyId): \Appwrite\Models\PolicyPasswordDictionary|\Appwrite\Models\PolicyPasswordHistory|\Appwrite\Models\PolicyPasswordStrength|\Appwrite\Models\PolicyPasswordPersonalData|\Appwrite\Models\PolicySessionAlert|\Appwrite\Models\PolicySessionDuration|\Appwrite\Models\PolicySessionInvalidation|\Appwrite\Models\PolicySessionLimit|\Appwrite\Models\PolicyUserLimit|\Appwrite\Models\PolicyMembershipPrivacy|\Appwrite\Models\PolicyMfaFactors|\Appwrite\Models\PolicyDenyAliasedEmail|\Appwrite\Models\PolicyDenyDisposableEmail|\Appwrite\Models\PolicyDenyFreeEmail|\Appwrite\Models\PolicyDenyCorporateEmail
public function getPolicy(ProjectPolicyId $policyId): \Appwrite\Models\PolicyPasswordDictionary|\Appwrite\Models\PolicyPasswordHistory|\Appwrite\Models\PolicyPasswordStrength|\Appwrite\Models\PolicyPasswordPersonalData|\Appwrite\Models\PolicySessionAlert|\Appwrite\Models\PolicySessionDuration|\Appwrite\Models\PolicySessionInvalidation|\Appwrite\Models\PolicySessionLimit|\Appwrite\Models\PolicyUserLimit|\Appwrite\Models\PolicyMembershipPrivacy|\Appwrite\Models\PolicyMfaFactors|\Appwrite\Models\PolicyDenyAliasedEmail|\Appwrite\Models\PolicyDenyDisposableEmail|\Appwrite\Models\PolicyDenyFreeEmail|\Appwrite\Models\PolicyDenyCorporateEmail|\Appwrite\Models\PolicyPasskey
{
$apiPath = str_replace(
['{policyId}'],
Expand Down Expand Up @@ -3850,6 +3850,10 @@ public function getPolicy(ProjectPolicyId $policyId): \Appwrite\Models\PolicyPas
return \Appwrite\Models\PolicyDenyCorporateEmail::from($response);
}

if (($response['$id'] ?? null) === 'passkey') {
return \Appwrite\Models\PolicyPasskey::from($response);
}

throw new \UnexpectedValueException('Unable to match response to any expected response model.');
}

Expand Down
7 changes: 4 additions & 3 deletions tests/Appwrite/Services/ProjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3208,10 +3208,11 @@ public function testMethodGetPolicy(): void
$data = array_replace(
[
"\$id" => "password-dictionary",
"enabled" => true
"rpId" => "example.com",
"origins" => []
],
[
"\$id" => "deny-corporate-email"
"\$id" => "passkey"
]
);

Expand All @@ -3226,7 +3227,7 @@ public function testMethodGetPolicy(): void
ProjectPolicyId::PASSWORDDICTIONARY()
);

$this->assertInstanceOf(\Appwrite\Models\PolicyDenyCorporateEmail::class, $response);
$this->assertInstanceOf(\Appwrite\Models\PolicyPasskey::class, $response);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test does not exercise the new passkey policy ID

The mocked response has $id = passkey, but this test still calls ProjectPolicyId::PASSWORDDICTIONARY() and does not assert the request path, so it passes even if PASSKEY() is missing or getPolicy(PASSKEY()) targets the wrong endpoint. It also replaces the prior deny-corporate-email hydration case; please exercise the passkey ID/path and retain coverage for that existing branch.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/Appwrite/Services/ProjectTest.php
Line: 3230

Comment:
**Test does not exercise the new passkey policy ID**

The mocked response has `$id = passkey`, but this test still calls `ProjectPolicyId::PASSWORDDICTIONARY()` and does not assert the request path, so it passes even if `PASSKEY()` is missing or `getPolicy(PASSKEY())` targets the wrong endpoint. It also replaces the prior deny-corporate-email hydration case; please exercise the passkey ID/path and retain coverage for that existing branch.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

🟡 Minor · testing · Reply if this doesn't apply.

}
public function testMethodUpdateProtocol(): void
{
Expand Down
Loading