Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
9d57630
Add ServerClient to web SDK with type-gated admin methods
ChiragAgg5k Apr 27, 2026
e6a42d1
Validate web server SDK build
ChiragAgg5k Apr 30, 2026
c241caa
Unify web client auth factories
ChiragAgg5k Apr 30, 2026
d1823d7
Address web client review feedback
ChiragAgg5k Apr 30, 2026
32058bf
Fix web client setter runtime behavior
ChiragAgg5k Apr 30, 2026
c71be4a
Preserve fluent setter mutation behavior
ChiragAgg5k Apr 30, 2026
7b644b0
Treat dev keys as browser auth
ChiragAgg5k Apr 30, 2026
a4bd8ff
Avoid unused web auth type imports
ChiragAgg5k Apr 30, 2026
7225395
Allow dual-platform web auth methods
ChiragAgg5k Apr 30, 2026
071a493
Avoid unused auth imports in dual services
ChiragAgg5k Apr 30, 2026
dfc32b2
Restore console auth factory options
ChiragAgg5k May 4, 2026
bfbfb17
Rename web auth type aliases by platform
ChiragAgg5k May 4, 2026
e70155e
Remove client-platform web runtime fallback
ChiragAgg5k May 4, 2026
65137fe
Clean examples before generation
ChiragAgg5k May 4, 2026
3eba0b2
Rename browser client factory
ChiragAgg5k May 4, 2026
f699aef
Fix web client runtime default
ChiragAgg5k May 4, 2026
000b946
Treat web JWT auth as server auth
ChiragAgg5k May 4, 2026
8dded00
refactor(web): address review feedback on typed client
ChiragAgg5k May 4, 2026
51446b5
formatting
ChiragAgg5k May 4, 2026
1dc15b2
refactor(web): move auth detection and this-gate logic into Twig filters
ChiragAgg5k May 4, 2026
2b73513
formatting
ChiragAgg5k May 4, 2026
73f9020
refactor(web): consolidate auth tiers into ClientAuth and ServerAuth
ChiragAgg5k May 4, 2026
7d2e645
refactor(web): guard fromJWT to server/console + expose selfSigned param
ChiragAgg5k May 4, 2026
a47f9d7
refactor(web): spec-drive client config and auth setters
ChiragAgg5k May 4, 2026
8907d83
fix(web): drop duplicate forwardeduseragent setter on server build
ChiragAgg5k May 4, 2026
73f0a60
Preserve legacy web client setters
ChiragAgg5k May 6, 2026
1b06c8d
Merge remote-tracking branch 'origin/master' into feat/web-isomorphic…
ChiragAgg5k May 6, 2026
fd3a3a6
Hide unauthorized web service methods
ChiragAgg5k May 6, 2026
982a36c
Merge remote-tracking branch 'origin/master' into feat/web-isomorphic…
ChiragAgg5k May 6, 2026
23661a6
Hide internal web client helpers
ChiragAgg5k May 6, 2026
294cc8b
Remove web client header workaround
ChiragAgg5k May 6, 2026
5c7b728
Generate web client base params from headers
ChiragAgg5k May 6, 2026
8427261
Extract web client setter return types
ChiragAgg5k May 6, 2026
4cf2907
Update web test auth headers fixture
ChiragAgg5k May 6, 2026
0472fb1
Add Flutter client factory surface
ChiragAgg5k May 6, 2026
5ae2b1c
Make Flutter factories independent of optional setters
ChiragAgg5k May 6, 2026
7e1e7bf
Address Flutter client surface review comments
ChiragAgg5k May 6, 2026
8c6d089
Add Apple client auth factories
ChiragAgg5k May 6, 2026
f81b78a
Address Apple factory review comments
ChiragAgg5k May 6, 2026
9483e43
Merge web isomorphic SDK changes
ChiragAgg5k May 7, 2026
10a3357
Merge Flutter isomorphic SDK changes
ChiragAgg5k May 7, 2026
8523d2f
Add Android client auth factories
ChiragAgg5k May 7, 2026
037a413
Fix Greptile isomorphic SDK review comments
ChiragAgg5k May 7, 2026
a833944
Use spec-driven Apple auth headers
ChiragAgg5k May 7, 2026
9ee0b7b
Add auth factory flow test coverage
ChiragAgg5k May 7, 2026
3fa8c8c
Add Flutter auth factory test coverage
ChiragAgg5k May 7, 2026
2b3ed33
Validate auth factory endpoint URLs
ChiragAgg5k May 7, 2026
e629fec
Enforce console cookie admin mode
ChiragAgg5k May 7, 2026
f9e6a47
Add auth factories to server SDK variants
ChiragAgg5k May 7, 2026
2c027e6
Tighten realtime auth factory surface
ChiragAgg5k May 7, 2026
a7ad1d9
Use SDK exception for Flutter impersonation validation
ChiragAgg5k May 7, 2026
d44a370
Standardize auth factory test expectations
ChiragAgg5k May 7, 2026
734572a
Expand isomorphic auth factory test coverage
ChiragAgg5k May 7, 2026
2bb7ce4
Fix web client API key factory
ChiragAgg5k May 7, 2026
a541ccd
Rename web auth receiver filter
ChiragAgg5k May 7, 2026
dce0f0e
Fix isomorphic auth template issues
ChiragAgg5k May 7, 2026
e3b701e
Fix web auth config typing
ChiragAgg5k May 7, 2026
0faf4d9
Mirror auth factory values into config
ChiragAgg5k May 7, 2026
b3f10bd
Mirror Swift auth factories into config
ChiragAgg5k May 7, 2026
f24d0be
Use Android client config property
ChiragAgg5k May 11, 2026
f5a8b33
Fix Android ClientAuth config override
ChiragAgg5k May 11, 2026
d16d121
Update Android auth factory tests for config property
ChiragAgg5k May 11, 2026
b10a74d
Return concrete Android client from factories
ChiragAgg5k May 11, 2026
4ef7ff4
Rename base auth factory to from
ChiragAgg5k May 11, 2026
d4cddd5
Store lowercase Flutter auth config aliases
ChiragAgg5k May 11, 2026
8abc2ef
refactor: address Android maintainer review comments
ChiragAgg5k May 12, 2026
01fda51
Merge remote-tracking branch 'origin/master' into feat/isomorphic-sdks
ChiragAgg5k May 13, 2026
0746067
Fix Swift CI auth factory tests
ChiragAgg5k May 13, 2026
f16ab5f
Merge branch 'master' into feat/isomorphic-sdks
ChiragAgg5k May 14, 2026
7a55bf3
Remove accidental Android Realtime conflict changes
ChiragAgg5k May 14, 2026
5c1973d
Limit React Native auth factories to client flows
ChiragAgg5k May 14, 2026
73c6137
Merge branch 'master' into feat/isomorphic-sdks
ChiragAgg5k May 15, 2026
68cb6fc
Fix web examples session auth
ChiragAgg5k May 15, 2026
344375a
Merge remote-tracking branch 'origin/master' into pr-1512
ChiragAgg5k May 19, 2026
909430a
Merge branch 'master' into feat/isomorphic-sdks
ChiragAgg5k May 19, 2026
1915d0c
Update docs examples for auth factories
ChiragAgg5k May 19, 2026
624439c
fix(web): default Client to client platform and stop leaking auth int…
ChiragAgg5k May 20, 2026
dda722d
fix(web): restore project ID to URL builders while keeping secrets out
ChiragAgg5k May 20, 2026
875dc7c
Merge branch 'master' into feat/isomorphic-sdks
ChiragAgg5k May 20, 2026
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
3 changes: 3 additions & 0 deletions .github/workflows/sdk-build-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ jobs:
platform: client

# Server SDKs
- sdk: web
platform: server

- sdk: node
platform: server

Expand Down
2 changes: 1 addition & 1 deletion src/SDK/Language/Apple.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public function getFiles(): array
[
'scope' => 'default',
'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Services/Service.swift',
'template' => 'swift/Sources/Service.swift.twig',
'template' => 'apple/Sources/Service.swift.twig',
],
[
'scope' => 'default',
Expand Down
2 changes: 1 addition & 1 deletion src/SDK/Language/Flutter.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function getFiles(): array
[
'scope' => 'default',
'destination' => '/lib/src/service.dart',
'template' => 'dart/lib/src/service.dart.twig',
'template' => 'flutter/lib/src/service.dart.twig',
],
[
'scope' => 'default',
Expand Down
180 changes: 180 additions & 0 deletions src/SDK/Language/Web.php
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,10 @@ public function getReturn(array $method, array $spec): string
}

if ($method['type'] === 'location') {
$platforms = $method['platforms'] ?? [];
if ((in_array('server', $platforms, true) || in_array('console', $platforms, true)) && !in_array('client', $platforms, true)) {
return 'Promise<ArrayBuffer>';
}
return 'string';
}

Expand Down Expand Up @@ -481,6 +485,167 @@ public function getSubSchema(array $property, array $spec, string $methodName =
return $this->getTypeName($property);
}

/**
* Determine whether a method supports client-side platforms.
*/
protected function methodSupportsClient(array $method): bool
{
return in_array('client', $method['platforms'] ?? [], true);
}

/**
* Determine whether a method supports server/console platforms.
*/
protected function methodSupportsServer(array $method): bool
{
$platforms = $method['platforms'] ?? [];
return in_array('server', $platforms, true) || in_array('console', $platforms, true);
}

/**
* Compute auth-related flags for a Web service.
*
* @return array<string, mixed>
*/
public function webServiceAuth(array $service): array
{
$hasClientTier = false;
$hasServerTier = false;
$hasServerOnly = false;
$hasClientOnly = false;
$hasUpload = false;
$needsServiceFlatten = false;
$serverOnlyMethods = [];
$clientOnlyMethods = [];

foreach ($service['methods'] ?? [] as $method) {
$hasClient = $this->methodSupportsClient($method);
$hasServer = $this->methodSupportsServer($method);
$methodName = $this->toCamelCase($method['name'] ?? '');

if ($hasClient) {
$hasClientTier = true;
}
if ($hasServer) {
$hasServerTier = true;
}
if ($hasServer && !$hasClient) {
$hasServerOnly = true;
$serverOnlyMethods[] = $methodName;
}
if ($hasClient && !$hasServer) {
$hasClientOnly = true;
$clientOnlyMethods[] = $methodName;
}
if (in_array('multipart/form-data', $method['consumes'] ?? [], true)) {
$hasUpload = true;
}
if (in_array($method['type'] ?? '', ['location', 'webAuth'], true)) {
$needsServiceFlatten = true;
}
}

$hasMixedTier = $hasClientTier && $hasServerTier;

return [
'hasClientTier' => $hasClientTier,
'hasServerTier' => $hasServerTier,
'hasMixedTier' => $hasMixedTier,
'needsServerAuth' => $hasServerTier && (!$hasMixedTier || $hasServerOnly),
'needsClientAuth' => $hasClientTier && (!$hasMixedTier || $hasClientOnly),
'hasUpload' => $hasUpload,
'needsServiceFlatten' => $needsServiceFlatten,
'serverOnlyMethods' => array_values(array_unique($serverOnlyMethods)),
'clientOnlyMethods' => array_values(array_unique($clientOnlyMethods)),
];
}

/**
* Build the Web client config keys required by the isomorphic auth factories.
*
* @return string[]
*/
public function webClientConfigKeys(array $headers): array
{
$keys = [];
foreach ($headers as $header) {
$keys[] = strtolower((string)($header['key'] ?? ''));
}

return array_values(array_unique(array_filter($keys)));
}

/**
* Build the TypeScript `this:` receiver constraint for a method in a Web service.
*/
public function webMethodAuthReceiver(array $method, array $service): string
{
$auth = $this->webServiceAuth($service);
if (!$auth['hasMixedTier']) {
return '';
}

$serviceName = $this->toPascalCase($service['name'] ?? '');

if (!$this->methodSupportsClient($method)) {
return 'this: ' . $serviceName . '<ServerAuth>, ';
}
if (!$this->methodSupportsServer($method)) {
return 'this: ' . $serviceName . '<ClientAuth>, ';
}

return '';
}

public function webClientBaseParams(array $headers): array
{
$params = [
'Project' => [
'name' => 'projectId',
'required' => true,
'setter' => 'setProject',
],
'Locale' => [
'name' => 'locale',
'required' => false,
'setter' => 'setLocale',
],
'Mode' => [
'name' => 'mode',
'required' => false,
'setter' => 'setMode',
],
'Platform' => [
'name' => 'platform',
'required' => false,
'setter' => 'setPlatform',
],
];

$baseParams = [];
foreach ($headers as $header) {
$key = $header['key'] ?? '';
if (isset($params[$key])) {
$baseParams[] = $params[$key];
}
}

return $baseParams;
}

public function webClientSetterReturnType(array $header): string
{
return match ($header['key'] ?? '') {
'Key' => "ClientRuntime<'apiKey'>",
'Cookie' => "ClientRuntime<'cookie'>",
'JWT' => "ClientRuntime<'jwt'>",
'Session' => "ClientRuntime<'session'>",
'DevKey' => "ClientRuntime<'devKey'>",
'ImpersonateUserId', 'ImpersonateUserEmail', 'ImpersonateUserPhone' => "ClientRuntime<'impersonation'>",
default => 'this',
};
}

public function getFilters(): array
{
return \array_merge(parent::getFilters(), [
Expand Down Expand Up @@ -541,6 +706,21 @@ public function getFilters(): array

return $condition;
}, ['is_safe' => ['html']]),
new TwigFilter('webServiceAuth', function (array $service) {
return $this->webServiceAuth($service);
}),
new TwigFilter('webMethodAuthReceiver', function (array $method, array $service) {
return $this->webMethodAuthReceiver($method, $service);
}, ['is_safe' => ['html']]),
new TwigFilter('webClientBaseParams', function (array $headers) {
return $this->webClientBaseParams($headers);
}),
new TwigFilter('webClientConfigKeys', function (array $headers) {
return $this->webClientConfigKeys($headers);
}),
new TwigFilter('webClientSetterReturnType', function (array $header) {
return $this->webClientSetterReturnType($header);
}, ['is_safe' => ['html']]),
new TwigFilter('comment2', function ($value) {
$value = explode("\n", $value);
foreach ($value as $key => $line) {
Expand Down
41 changes: 31 additions & 10 deletions templates/android/docs/java/example.md.twig
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,37 @@ import {{ sdk.namespace | caseDot }}.enums.{{ parameter.enumName | caseUcfirst }
{% endif %}
{% endfor %}

Client client = new Client(context)
{%~ if method.auth|length > 0 %}
.setEndpoint("{{ spec.endpointDocs | raw }}") // Your API Endpoint
{%~ for node in method.auth %}
{%~ for key,header in node|keys %}
.set{{header | caseUcfirst}}("{{node[header]['x-appwrite']['demo'] | raw }}"){% if loop.last %};{% endif %} // {{ node[header].description }}
{%~ endfor %}
{%~ endfor %}
{%~ endif %}
{% set authNode = {} %}
{% for node in method.auth %}
{% set authNode = authNode|merge(node) %}
{% endfor %}
{% set authHeaders = [] %}
{% for header in authNode|keys %}
{% if not (header == 'Session' and 'client' in method.platforms) %}
{% set authHeaders = authHeaders|merge([header]) %}
{% endif %}
{% endfor %}
{% set factory = 'from' %}
{% if 'ImpersonateUserId' in authHeaders or 'ImpersonateUserEmail' in authHeaders or 'ImpersonateUserPhone' in authHeaders %}{% set factory = 'fromImpersonation' %}{% elseif 'DevKey' in authHeaders %}{% set factory = 'fromDevKey' %}{% elseif 'Session' in authHeaders %}{% set factory = 'fromSession' %}{% endif %}
{% if method.auth|length > 0 %}
Client client = Client.{{ factory }}(
context,
"{{ authNode.Project['x-appwrite']['demo'] | raw }}", // Your project ID
{% if factory == 'fromSession' or factory == 'fromImpersonation' %}
"{{ authNode.Session['x-appwrite']['demo'] | raw }}", // {{ authNode.Session.description }}
{% elseif factory == 'fromDevKey' %}
"{{ authNode.DevKey['x-appwrite']['demo'] | raw }}", // {{ authNode.DevKey.description }}
{% endif %}
{% if factory == 'fromImpersonation' %}
{% if 'ImpersonateUserId' in authHeaders %}"{{ authNode.ImpersonateUserId['x-appwrite']['demo'] | raw }}", // {{ authNode.ImpersonateUserId.description }}{% else %}null,{% endif %}
{% if 'ImpersonateUserEmail' in authHeaders %}"{{ authNode.ImpersonateUserEmail['x-appwrite']['demo'] | raw }}", // {{ authNode.ImpersonateUserEmail.description }}{% else %}null,{% endif %}
{% if 'ImpersonateUserPhone' in authHeaders %}"{{ authNode.ImpersonateUserPhone['x-appwrite']['demo'] | raw }}", // {{ authNode.ImpersonateUserPhone.description }}{% else %}null,{% endif %}
{% endif %}
"{{ spec.endpointDocs | raw }}" // Your API Endpoint
);
{% else %}
Client client = new Client(context);
{% endif %}

{{ service.name | caseUcfirst }} {{ service.name | caseCamel }} = new {{ service.name | caseUcfirst }}(client);

Expand Down Expand Up @@ -56,6 +78,5 @@ Client client = new Client(context)
})
);
{% endif %}

{% endfor %}
```
33 changes: 25 additions & 8 deletions templates/android/docs/kotlin/example.md.twig
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,32 @@ import {{ sdk.namespace | caseDot }}.Permission
import {{ sdk.namespace | caseDot }}.Role
{% endif %}

{% set authNode = {} %}
{% for node in method.auth %}
{% set authNode = authNode|merge(node) %}
{% endfor %}
{% set authHeaders = [] %}
{% for header in authNode|keys %}
{% if not (header == 'Session' and 'client' in method.platforms) %}
{% set authHeaders = authHeaders|merge([header]) %}
{% endif %}
{% endfor %}
{% set factory = 'from' %}
{% if 'ImpersonateUserId' in authHeaders or 'ImpersonateUserEmail' in authHeaders or 'ImpersonateUserPhone' in authHeaders %}{% set factory = 'fromImpersonation' %}{% elseif 'DevKey' in authHeaders %}{% set factory = 'fromDevKey' %}{% elseif 'Session' in authHeaders %}{% set factory = 'fromSession' %}{% endif %}
{% if method.auth|length > 0 %}
val client = Client.{{ factory }}(
context = context,
endpoint = "{{ spec.endpointDocs | raw }}", // Your API Endpoint
projectId = "{{ authNode.Project['x-appwrite']['demo'] | raw }}"{% if authHeaders|length > 1 %},{% endif %} // Your project ID
{% for header in authHeaders %}
{% if header != 'Project' %}
{% if header == 'Session' %}session{% elseif header == 'DevKey' %}devKey{% elseif header == 'ImpersonateUserId' %}userId{% elseif header == 'ImpersonateUserEmail' %}userEmail{% elseif header == 'ImpersonateUserPhone' %}userPhone{% else %}{{ header | caseCamel }}{% endif %} = "{{ authNode[header]['x-appwrite']['demo'] | raw }}"{% if not loop.last %},{% endif %} // {{ authNode[header].description }}
{% endif %}
{% endfor %}
)
{% else %}
val client = Client(context)
{%~ if method.auth|length > 0 %}
.setEndpoint("{{ spec.endpointDocs | raw }}") // Your API Endpoint
{%~ for node in method.auth %}
{%~ for key,header in node|keys %}
.set{{header | caseUcfirst}}("{{node[header]['x-appwrite']['demo'] | raw }}") // {{node[header].description}}
{%~ endfor %}
{%~ endfor %}
{%~ endif %}
{% endif %}

val {{ service.name | caseCamel }} = {{ service.name | caseUcfirst }}(client)

Expand Down
Loading
Loading