From 9d576307d8e1745f0032782fd6977c2ad18700bb Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 27 Apr 2026 16:40:57 +0530 Subject: [PATCH 01/69] Add ServerClient to web SDK with type-gated admin methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a ServerClient sibling class to the web SDK alongside the existing Client. Service classes are generic over `Client | ServerClient` when they have any client-tier methods, with TypeScript `this`-types gating admin methods (e.g. `Databases.createCollection` requires `Databases`). Services with no client-tier methods (Health, Tokens, Sites, Users) are non-generic and require a ServerClient at construction. Tier detection is driven entirely off existing `x-appwrite.platforms` spec tags. The existing Client surface is unchanged — purely additive for current `appwrite` web users; sets up the path to consolidate `appwrite` + `node-appwrite` into a single isomorphic package. - Filter `Key` out of Client header iterations so Client cannot setKey - Add server-client.ts.twig (setKey/setJWT/setLocale + HTTP plumbing, no realtime, no session/devkey/impersonate) - Type-gate service methods via `this: Service` (or `` for the few client-only methods like webAuth/location) - Re-export ServerClient from index.ts - Register the new template in Web.php getFiles() Verified on regenerated examples/web/: tsc --noEmit passes; negative tests confirm `new Health(browserClient)` and admin calls on a Client-bound service fail to type-check; djlint passes. --- src/SDK/Language/Web.php | 5 + templates/web/src/client.ts.twig | 11 +- templates/web/src/index.ts.twig | 1 + templates/web/src/server-client.ts.twig | 291 ++++++++++++++++++++ templates/web/src/services/template.ts.twig | 58 +++- 5 files changed, 356 insertions(+), 10 deletions(-) create mode 100644 templates/web/src/server-client.ts.twig diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index 28002b7c09..e464a3ed2f 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -45,6 +45,11 @@ public function getFiles(): array 'destination' => 'src/client.ts', 'template' => 'web/src/client.ts.twig', ], + [ + 'scope' => 'default', + 'destination' => 'src/server-client.ts', + 'template' => 'web/src/server-client.ts.twig', + ], [ 'scope' => 'default', 'destination' => 'src/service.ts', diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index cff84095f6..926e7946a8 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -353,7 +353,7 @@ class Client { config: { endpoint: string; endpointRealtime: string; -{%~ for header in spec.global.headers %} +{%~ for header in spec.global.headers | filter(h => h.key|lower != 'key') %} {{ header.key | caseLower }}: string; {%~ endfor %} {%~ if sdk.platform == 'console' %} @@ -363,7 +363,7 @@ class Client { } = { endpoint: '{{ spec.endpoint }}', endpointRealtime: '', -{%~ for header in spec.global.headers %} +{%~ for header in spec.global.headers | filter(h => h.key|lower != 'key') %} {{ header.key | caseLower }}: '', {%~ endfor %} {%~ if sdk.platform == 'console' %} @@ -454,7 +454,7 @@ class Client { } {%~ endif %} - {%~ for header in spec.global.headers %} + {%~ for header in spec.global.headers | filter(h => h.key|lower != 'key') %} /** * Set {{header.key | caseUcfirst}} * @@ -833,13 +833,14 @@ class Client { end = file.size; // Adjust for the last chunk to include the last byte } - headers['content-range'] = `bytes ${start}-${end-1}/${file.size}`; + const chunkHeaders = { ...headers }; + chunkHeaders['content-range'] = `bytes ${start}-${end-1}/${file.size}`; const chunk = file.slice(start, end); let payload = { ...originalPayload }; payload[fileParam] = new File([chunk], file.name); - response = await this.call(method, url, headers, payload); + response = await this.call(method, url, chunkHeaders, payload); if (onProgress && typeof onProgress === 'function') { onProgress({ diff --git a/templates/web/src/index.ts.twig b/templates/web/src/index.ts.twig index c8595159f6..5f028bd108 100644 --- a/templates/web/src/index.ts.twig +++ b/templates/web/src/index.ts.twig @@ -6,6 +6,7 @@ * [previous releases](https://github.com/{{sdk.gitUserName}}/{{sdk.gitRepoName}}/releases). */ export { Client, Query, {{spec.title | caseUcfirst}}Exception } from './client'; +export { ServerClient } from './server-client'; {% for service in spec.services %} export { {{service.name | caseUcfirst}} } from './services/{{service.name | caseKebab}}'; {% endfor %} diff --git a/templates/web/src/server-client.ts.twig b/templates/web/src/server-client.ts.twig new file mode 100644 index 0000000000..6cc68e7b7c --- /dev/null +++ b/templates/web/src/server-client.ts.twig @@ -0,0 +1,291 @@ +import { {{spec.title | caseUcfirst}}Exception, JSONbig, type Payload, type UploadProgress } from './client'; +import { Service } from './service'; + +/** + * Headers type representing a key-value pair with string keys and string values. + */ +type Headers = { + [key: string]: string; +} + +{% set serverHeaderExcludes = [ + 'session', + 'dev-key', + 'devkey', + 'impersonate-user-id', + 'impersonateuserid', + 'impersonate-user-email', + 'impersonateuseremail', + 'impersonate-user-phone', + 'impersonateuserphone' +] %} +{% set serverKeyFlag = [] %} +{% for header in spec.global.headers %} +{% if header.key|lower == 'key' %} +{% set serverKeyFlag = serverKeyFlag|merge([1]) %} +{% endif %} +{% endfor %} +{% set serverHasKey = serverKeyFlag|length > 0 %} +{% set serverFallbackKeyHeader = 'X-' ~ (spec.title | caseUcfirst) ~ '-Key' %} + +/** + * ServerClient that handles requests to {{spec.title | caseUcfirst}} from trusted server + * runtimes. Authenticates with an API key (optionally a JWT) and exposes admin-tier + * methods on the shared service classes through TypeScript type-gating. + * + * Use {@link Client} for browser/end-user contexts. ServerClient does not support + * realtime subscriptions or browser-only credentials (session, dev key, impersonation). + */ +class ServerClient { + static CHUNK_SIZE = 1024 * 1024 * 5; + + /** + * Holds configuration such as project and API key. + */ + config: { + endpoint: string; +{%~ if not serverHasKey %} + key: string; +{%~ endif %} +{%~ for header in spec.global.headers | filter(h => h.key|lower not in serverHeaderExcludes) %} + {{ header.key | caseLower }}: string; +{%~ endfor %} + } = { + endpoint: '{{ spec.endpoint }}', +{%~ if not serverHasKey %} + key: '', +{%~ endif %} +{%~ for header in spec.global.headers | filter(h => h.key|lower not in serverHeaderExcludes) %} + {{ header.key | caseLower }}: '', +{%~ endfor %} + }; + + /** + * Custom headers for API requests. + */ + headers: Headers = { + 'x-sdk-name': '{{ sdk.name }}', + 'x-sdk-platform': 'server', + 'x-sdk-language': '{{ language.name | caseLower }}', + 'x-sdk-version': '{{ sdk.version }}', + {%~ for key,header in spec.global.defaultHeaders %} + '{{key}}': '{{header}}', + {%~ endfor %} + }; + + /** + * Get Headers + * + * Returns a copy of the current request headers, including any + * authentication headers. Handle with care. + * + * @returns {Headers} + */ + getHeaders(): Headers { + return { ...this.headers }; + } + + /** + * Set Endpoint + * + * Your project endpoint + * + * @param {string} endpoint + * + * @returns {this} + */ + setEndpoint(endpoint: string): this { + if (!endpoint || typeof endpoint !== 'string') { + throw new {{spec.title | caseUcfirst}}Exception('Endpoint must be a valid string'); + } + + if (!endpoint.startsWith('http://') && !endpoint.startsWith('https://')) { + throw new {{spec.title | caseUcfirst}}Exception('Invalid endpoint URL: ' + endpoint); + } + + this.config.endpoint = endpoint; + return this; + } + + {%~ for header in spec.global.headers | filter(h => h.key|lower not in serverHeaderExcludes) %} + /** + * Set {{header.key | caseUcfirst}} + * + {%~ if header.description %} + * {{header.description}} + * + {%~ endif %} + * @param value string + * + * @return {this} + */ + set{{header.key | caseUcfirst}}(value: string): this { + this.headers['{{header.name}}'] = value; + this.config.{{ header.key | caseLower }} = value; + return this; + } + {%~ endfor %} +{%~ if not serverHasKey %} + + /** + * Set Key + * + * Your secret API key. + * + * @param value string + * + * @return {this} + */ + setKey(value: string): this { + this.headers['{{ serverFallbackKeyHeader }}'] = value; + this.config.key = value; + return this; + } +{%~ endif %} + + prepareRequest(method: string, url: URL, headers: Headers = {}, params: Payload = {}): { uri: string, options: RequestInit } { + method = method.toUpperCase(); + + headers = Object.assign({}, this.headers, headers); + + let options: RequestInit = { + method, + headers, + }; + + if (method === 'GET') { + for (const [key, value] of Object.entries(Service.flatten(params))) { + url.searchParams.append(key, value); + } + } else { + switch (headers['content-type']) { + case 'application/json': + options.body = JSONbig.stringify(params); + break; + + case 'multipart/form-data': + if (typeof FormData === 'undefined' || typeof File === 'undefined') { + throw new {{spec.title | caseUcfirst}}Exception('Multipart requests require File and FormData globals'); + } + + const formData = new FormData(); + + for (const [key, value] of Object.entries(params)) { + if (value instanceof File) { + formData.append(key, value, value.name); + } else if (Array.isArray(value)) { + for (const nestedValue of value) { + formData.append(`${key}[]`, nestedValue); + } + } else { + formData.append(key, value); + } + } + + options.body = formData; + delete headers['content-type']; + break; + } + } + + return { uri: url.toString(), options }; + } + + async chunkedUpload(method: string, url: URL, headers: Headers = {}, originalPayload: Payload = {}, onProgress: (progress: UploadProgress) => void) { + if (typeof File === 'undefined' || typeof FormData === 'undefined') { + throw new {{spec.title | caseUcfirst}}Exception('Chunked uploads require File and FormData globals'); + } + + const [fileParam, file] = Object.entries(originalPayload).find(([_, value]) => value instanceof File) ?? []; + + if (!file || !fileParam) { + throw new Error('File not found in payload'); + } + + if (file.size <= ServerClient.CHUNK_SIZE) { + return await this.call(method, url, headers, originalPayload); + } + + let start = 0; + let response = null; + + while (start < file.size) { + let end = start + ServerClient.CHUNK_SIZE; + if (end >= file.size) { + end = file.size; + } + + const chunkHeaders = { ...headers }; + chunkHeaders['content-range'] = `bytes ${start}-${end-1}/${file.size}`; + const chunk = file.slice(start, end); + + let payload = { ...originalPayload }; + payload[fileParam] = new File([chunk], file.name); + + response = await this.call(method, url, chunkHeaders, payload); + + if (onProgress && typeof onProgress === 'function') { + onProgress({ + $id: response.$id, + progress: Math.round((end / file.size) * 100), + sizeUploaded: end, + chunksTotal: Math.ceil(file.size / ServerClient.CHUNK_SIZE), + chunksUploaded: Math.ceil(end / ServerClient.CHUNK_SIZE) + }); + } + + if (response && response.$id) { + headers['x-{{spec.title | caseLower }}-id'] = response.$id; + } + + start = end; + } + + return response; + } + + async ping(): Promise { + return this.call('GET', new URL(this.config.endpoint + '/ping')); + } + + async call(method: string, url: URL, headers: Headers = {}, params: Payload = {}, responseType = 'json'): Promise { + const { uri, options } = this.prepareRequest(method, url, headers, params); + + let data: any = null; + + const response = await fetch(uri, options); + + const warnings = response.headers.get('x-{{ spec.title | lower }}-warning'); + if (warnings) { + warnings.split(';').forEach((warning: string) => console.warn('Warning: ' + warning)); + } + + if (response.headers.get('content-type')?.includes('application/json')) { + data = JSONbig.parse(await response.text()); + } else if (responseType === 'arrayBuffer') { + data = await response.arrayBuffer(); + } else { + data = { + message: await response.text() + }; + } + + if (400 <= response.status) { + let responseText = ''; + if (response.headers.get('content-type')?.includes('application/json') || responseType === 'arrayBuffer') { + responseText = JSONbig.stringify(data); + } else { + responseText = data?.message; + } + throw new {{spec.title | caseUcfirst}}Exception(data?.message, response.status, data?.type, responseText); + } + + if (data && typeof data === 'object') { + data.toString = () => JSONbig.stringify(data); + } + + return data; + } +} + +export { ServerClient }; diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index 4066c3f600..bc827466ef 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -1,5 +1,27 @@ +{# Detect service shape and imports before emitting TypeScript. #} +{% set tierFlag = [] %} +{% set serverFlag = [] %} +{% set uploadFlag = [] %} +{% for m in service.methods %} +{% if 'client' in m.platforms %} +{% set tierFlag = tierFlag|merge([1]) %} +{% endif %} +{% if 'server' in m.platforms or 'console' in m.platforms %} +{% set serverFlag = serverFlag|merge([1]) %} +{% endif %} +{% if 'multipart/form-data' in m.consumes %} +{% set uploadFlag = uploadFlag|merge([1]) %} +{% endif %} +{% endfor %} +{% set hasClientTier = tierFlag|length > 0 %} +{% set hasServerTier = serverFlag|length > 0 %} +{% set hasMixedTier = hasClientTier and hasServerTier %} +{% set hasUpload = uploadFlag|length > 0 %} import { Service } from '../service'; -import { {{ spec.title | caseUcfirst}}Exception, Client, type Payload, UploadProgress } from '../client'; +import { {{ spec.title | caseUcfirst}}Exception, {% if hasClientTier %}Client, {% endif %}type Payload{% if hasUpload %}, UploadProgress{% endif %} } from '../client'; +{%~ if hasServerTier %} +import type { ServerClient } from '../server-client'; +{%~ endif %} import type { Models } from '../models'; {% set added = [] %} @@ -14,14 +36,40 @@ import { {{ parameter.enumName | caseUcfirst }} } from '../enums/{{ parameter.en {% endfor %} {% endfor %} +{%~ if hasMixedTier %} +export class {{ service.name | caseUcfirst }} { + client: TClient; + + constructor(client: TClient) { + this.client = client; + } +{%~ elseif hasServerTier %} +export class {{ service.name | caseUcfirst }} { + client: ServerClient; + + constructor(client: ServerClient) { + this.client = client; + } +{%~ else %} export class {{ service.name | caseUcfirst }} { client: Client; constructor(client: Client) { this.client = client; } +{%~ endif %} {%~ for method in service.methods %} + {%~ set thisGate = '' %} + {%~ set methodSupportsClient = 'client' in method.platforms %} + {%~ set methodSupportsServer = 'server' in method.platforms or 'console' in method.platforms %} + {%~ if hasMixedTier %} + {%~ if not methodSupportsClient %} + {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} + {%~ elseif not methodSupportsServer %} + {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} + {%~ endif %} + {%~ endif %} /** {%~ if method.description %} * {{ method.description | replace({'\n': '\n * '}) | raw }} @@ -41,7 +89,7 @@ export class {{ service.name | caseUcfirst }} { {%~ endif %} */ {%~ if method.parameters.all|length > 0 %} - {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}(params{% set hasRequiredParams = false %}{% for parameter in method.parameters.all %}{% if parameter.required %}{% set hasRequiredParams = true %}{% endif %}{% endfor %}{% if not hasRequiredParams %}?{% endif %}: { {% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress?: (progress: UploadProgress) => void{% endif %} }): {{ method | getReturn(spec) | raw }}; + {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({{ thisGate | raw }}params{% set hasRequiredParams = false %}{% for parameter in method.parameters.all %}{% if parameter.required %}{% set hasRequiredParams = true %}{% endif %}{% endfor %}{% if not hasRequiredParams %}?{% endif %}: { {% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress?: (progress: UploadProgress) => void{% endif %} }): {{ method | getReturn(spec) | raw }}; /** {%~ if method.description %} * {{ method.description | replace({'\n': '\n * '}) | raw }} @@ -54,9 +102,9 @@ export class {{ service.name | caseUcfirst }} { * @returns {{ '{' }}{{ method | getReturn(spec) | raw }}{{ '}' }} * @deprecated Use the object parameter style method for a better developer experience. */ - {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress?: (progress: UploadProgress) => void{% endif %}): {{ method | getReturn(spec) | raw }}; + {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({{ thisGate | raw }}{% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress?: (progress: UploadProgress) => void{% endif %}): {{ method | getReturn(spec) | raw }}; {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}( - {% if method.parameters.all|length > 0 %}paramsOrFirst{% if not method.parameters.all[0].required or method.parameters.all[0].nullable %}?{% endif %}: { {% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress?: (progress: UploadProgress) => void{% endif %} } | {{ method.parameters.all[0] | getPropertyType(method) | raw }}{% if method.parameters.all|length > 1 %}, + {{ thisGate | raw }}{% if method.parameters.all|length > 0 %}paramsOrFirst{% if not method.parameters.all[0].required or method.parameters.all[0].nullable %}?{% endif %}: { {% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress?: (progress: UploadProgress) => void{% endif %} } | {{ method.parameters.all[0] | getPropertyType(method) | raw }}{% if method.parameters.all|length > 1 %}, ...rest: [{% for parameter in method.parameters.all[1:] %}({{ parameter | getPropertyType(method) | raw }})?{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %},((progress: UploadProgress) => void)?{% endif %}]{% endif %}{% endif %} ): {{ method | getReturn(spec) | raw }} { @@ -96,7 +144,7 @@ export class {{ service.name | caseUcfirst }} { {%~ endif %} {%~ endif %} {%~ else %} - {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress = (progress: UploadProgress) => void{% endif %}): {{ method | getReturn(spec) | raw }} { + {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({{ thisGate | raw }}{% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress = (progress: UploadProgress) => void{% endif %}): {{ method | getReturn(spec) | raw }} { {%~ endif %} {%~ for parameter in method.parameters.all %} {%~ if parameter.required %} From e6a42d176d97a71b5aefbab52402ec5b7d626ddd Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 30 Apr 2026 14:58:55 +0530 Subject: [PATCH 02/69] Validate web server SDK build --- .github/workflows/sdk-build-validation.yml | 3 +++ templates/web/src/services/template.ts.twig | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sdk-build-validation.yml b/.github/workflows/sdk-build-validation.yml index c5ac910d43..28637ac25b 100644 --- a/.github/workflows/sdk-build-validation.yml +++ b/.github/workflows/sdk-build-validation.yml @@ -37,6 +37,9 @@ jobs: platform: client # Server SDKs + - sdk: web + platform: server + - sdk: node platform: server diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index bc827466ef..7f8119bc63 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -64,7 +64,11 @@ export class {{ service.name | caseUcfirst }} { {%~ set methodSupportsClient = 'client' in method.platforms %} {%~ set methodSupportsServer = 'server' in method.platforms or 'console' in method.platforms %} {%~ if hasMixedTier %} - {%~ if not methodSupportsClient %} + {%~ if (method.type == 'location' or method.type == 'webAuth') and not methodSupportsClient %} + {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} + {%~ elseif method.type == 'location' or method.type == 'webAuth' %} + {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} + {%~ elseif not methodSupportsClient %} {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} {%~ elseif not methodSupportsServer %} {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} From c241caaf5a199046df2388709112b16fdb1010f9 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 30 Apr 2026 16:03:15 +0530 Subject: [PATCH 03/69] Unify web client auth factories --- src/SDK/Language/Web.php | 5 - templates/web/src/client.ts.twig | 268 +++++++++++++++--- templates/web/src/index.ts.twig | 5 +- templates/web/src/server-client.ts.twig | 291 -------------------- templates/web/src/services/realtime.ts.twig | 6 +- templates/web/src/services/template.ts.twig | 27 +- 6 files changed, 248 insertions(+), 354 deletions(-) delete mode 100644 templates/web/src/server-client.ts.twig diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index e464a3ed2f..28002b7c09 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -45,11 +45,6 @@ public function getFiles(): array 'destination' => 'src/client.ts', 'template' => 'web/src/client.ts.twig', ], - [ - 'scope' => 'default', - 'destination' => 'src/server-client.ts', - 'template' => 'web/src/server-client.ts.twig', - ], [ 'scope' => 'default', 'destination' => 'src/service.ts', diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 926e7946a8..6ce17354d4 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -344,7 +344,45 @@ class {{spec.title | caseUcfirst}}Exception extends Error { /** * Client that handles requests to {{spec.title | caseUcfirst}} */ -class Client { +type ClientRuntime = 'browser' | 'server'; +type ClientAuth = 'anonymous' | 'session' | 'jwt' | 'apiKey' | 'devKey' | 'impersonation'; +type AdminAuth = 'apiKey' | 'devKey'; +type BrowserAuth = 'anonymous' | 'session' | 'jwt' | 'impersonation'; +type ServerClient = Client; + +type BaseClientParams = { + endpoint: string; + projectId: string; + endpointRealtime?: string; + locale?: string; +}; + +type SessionClientParams = BaseClientParams & { + session: string; +}; + +type ApiKeyClientParams = BaseClientParams & { + apiKey: string; + jwt?: string; + forwardedUserAgent?: string; +}; + +type JWTClientParams = BaseClientParams & { + jwt: string; +}; + +type DevKeyClientParams = BaseClientParams & { + devKey: string; +}; + +type ImpersonationTarget = + | { userId: string; email?: never; phone?: never } + | { email: string; userId?: never; phone?: never } + | { phone: string; userId?: never; email?: never }; + +type ImpersonationClientParams = SessionClientParams & ImpersonationTarget; + +class Client { static CHUNK_SIZE = 1024 * 1024 * 5; /** @@ -353,24 +391,37 @@ class Client { config: { endpoint: string; endpointRealtime: string; -{%~ for header in spec.global.headers | filter(h => h.key|lower != 'key') %} - {{ header.key | caseLower }}: string; -{%~ endfor %} -{%~ if sdk.platform == 'console' %} + project: string; + key: string; + jwt: string; + locale: string; + session: string; + devkey: string; + forwardeduseragent: string; + impersonateuserid: string; + impersonateuseremail: string; + impersonateuserphone: string; selfSigned: boolean; - session?: string; -{%~ endif %} } = { endpoint: '{{ spec.endpoint }}', endpointRealtime: '', -{%~ for header in spec.global.headers | filter(h => h.key|lower != 'key') %} - {{ header.key | caseLower }}: '', -{%~ endfor %} -{%~ if sdk.platform == 'console' %} + project: '', + key: '', + jwt: '', + locale: '', + session: '', + devkey: '', + forwardeduseragent: '', + impersonateuserid: '', + impersonateuseremail: '', + impersonateuserphone: '', selfSigned: false, - session: undefined, -{%~ endif %} }; + + private runtime: ClientRuntime = '{{ sdk.platform == 'server' ? 'server' : 'browser' }}'; + private auth: ClientAuth = 'anonymous'; + private readonly authType?: TAuth; + /** * Custom headers for API requests. */ @@ -384,6 +435,91 @@ class Client { {%~ endfor %} }; + static anonymous(params: BaseClientParams): Client<'anonymous'> { + return new Client<'anonymous'>().applyBase(params, 'anonymous', 'browser'); + } + + static fromAnonymous(params: BaseClientParams): Client<'anonymous'> { + return Client.anonymous(params); + } + + static fromSession(params: SessionClientParams): Client<'session'> { + return new Client<'session'>() + .applyBase(params, 'session', 'browser') + .setSession(params.session); + } + + static fromApiKey(params: ApiKeyClientParams): Client<'apiKey'> { + const client = new Client<'apiKey'>() + .applyBase(params, 'apiKey', 'server') + .setKey(params.apiKey); + + if (params.jwt !== undefined) { + client.setJWT(params.jwt); + } + + if (params.forwardedUserAgent !== undefined) { + client.setForwardedUserAgent(params.forwardedUserAgent); + } + + return client; + } + + static fromJWT(params: JWTClientParams): Client<'jwt'> { + return new Client<'jwt'>() + .applyBase(params, 'jwt', 'browser') + .setJWT(params.jwt); + } + + static fromDevKey(params: DevKeyClientParams): Client<'devKey'> { + return new Client<'devKey'>() + .applyBase(params, 'devKey', 'server') + .setDevKey(params.devKey); + } + + static fromImpersonation(params: ImpersonationClientParams): Client<'impersonation'> { + const targets = [ + params.userId !== undefined, + params.email !== undefined, + params.phone !== undefined + ].filter(Boolean).length; + + if (targets !== 1) { + throw new {{spec.title | caseUcfirst}}Exception('Exactly one impersonation target must be provided'); + } + + const client = new Client<'impersonation'>() + .applyBase(params, 'impersonation', 'browser') + .setSession(params.session); + + if (params.userId !== undefined) { + return client.setImpersonateUserId(params.userId); + } + if (params.email !== undefined) { + return client.setImpersonateUserEmail(params.email); + } + return client.setImpersonateUserPhone(params.phone); + } + + private applyBase(params: BaseClientParams, auth: T, runtime: ClientRuntime): Client { + const client = this as unknown as Client; + client.auth = auth; + client.runtime = runtime; + client.headers['x-sdk-platform'] = runtime === 'server' ? 'server' : 'client'; + client.setEndpoint(params.endpoint); + client.setProject(params.projectId); + + if (params.endpointRealtime !== undefined) { + client.setEndpointRealtime(params.endpointRealtime); + } + + if (params.locale !== undefined) { + client.setLocale(params.locale); + } + + return client; + } + /** * Get Headers * @@ -440,7 +576,6 @@ class Client { return this; } -{%~ if sdk.platform == 'console' %} /** * Set self-signed * @@ -453,25 +588,77 @@ class Client { return this; } -{%~ endif %} - {%~ for header in spec.global.headers | filter(h => h.key|lower != 'key') %} - /** - * Set {{header.key | caseUcfirst}} - * - {%~ if header.description %} - * {{header.description}} - * - {%~ endif %} - * @param value string - * - * @return {this} - */ - set{{header.key | caseUcfirst}}(value: string): this { - this.headers['{{header.name}}'] = value; - this.config.{{ header.key | caseLower }} = value; + setProject(value: string): this { + this.headers['X-{{ spec.title | caseUcfirst }}-Project'] = value; + this.config.project = value; + return this; + } + + setKey(value: string): Client<'apiKey'> { + this.headers['X-{{ spec.title | caseUcfirst }}-Key'] = value; + this.config.key = value; + this.auth = 'apiKey'; + this.runtime = 'server'; + this.headers['x-sdk-platform'] = 'server'; + return this as unknown as Client<'apiKey'>; + } + + setJWT(value: string): this { + this.headers['X-{{ spec.title | caseUcfirst }}-JWT'] = value; + this.config.jwt = value; + return this; + } + + setLocale(value: string): this { + this.headers['X-{{ spec.title | caseUcfirst }}-Locale'] = value; + this.config.locale = value; + return this; + } + + setSession(value: string): Client<'session'> { + this.headers['X-{{ spec.title | caseUcfirst }}-Session'] = value; + this.config.session = value; + this.auth = 'session'; + this.runtime = 'browser'; + this.headers['x-sdk-platform'] = 'client'; + return this as unknown as Client<'session'>; + } + + setDevKey(value: string): Client<'devKey'> { + this.headers['X-{{ spec.title | caseUcfirst }}-Dev-Key'] = value; + this.config.devkey = value; + this.auth = 'devKey'; + this.runtime = 'server'; + this.headers['x-sdk-platform'] = 'server'; + return this as unknown as Client<'devKey'>; + } + + setForwardedUserAgent(value: string): this { + this.headers['X-Forwarded-User-Agent'] = value; + this.config.forwardeduseragent = value; return this; } - {%~ endfor %} + + setImpersonateUserId(value: string): Client<'impersonation'> { + this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id'] = value; + this.config.impersonateuserid = value; + this.auth = 'impersonation'; + return this as unknown as Client<'impersonation'>; + } + + setImpersonateUserEmail(value: string): Client<'impersonation'> { + this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email'] = value; + this.config.impersonateuseremail = value; + this.auth = 'impersonation'; + return this as unknown as Client<'impersonation'>; + } + + setImpersonateUserPhone(value: string): Client<'impersonation'> { + this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone'] = value; + this.config.impersonateuserphone = value; + this.auth = 'impersonation'; + return this as unknown as Client<'impersonation'>; + } private realtime: Realtime = { socket: undefined, @@ -763,7 +950,7 @@ class Client { headers = Object.assign({}, this.headers, headers); - if (typeof window !== 'undefined' && window.localStorage) { + if (this.runtime === 'browser' && typeof window !== 'undefined' && window.localStorage) { const cookieFallback = window.localStorage.getItem('cookieFallback'); if (cookieFallback) { headers['X-Fallback-Cookies'] = cookieFallback; @@ -775,7 +962,7 @@ class Client { headers, }; - if (headers['X-Appwrite-Dev-Key'] === undefined) { + if (this.runtime === 'browser' && headers['X-Appwrite-Dev-Key'] === undefined) { options.credentials = 'include'; } @@ -790,6 +977,10 @@ class Client { break; case 'multipart/form-data': + if (typeof FormData === 'undefined' || typeof File === 'undefined') { + throw new {{spec.title | caseUcfirst}}Exception('Multipart requests require File and FormData globals'); + } + const formData = new FormData(); for (const [key, value] of Object.entries(params)) { @@ -814,6 +1005,10 @@ class Client { } async chunkedUpload(method: string, url: URL, headers: Headers = {}, originalPayload: Payload = {}, onProgress: (progress: UploadProgress) => void) { + if (typeof File === 'undefined' || typeof FormData === 'undefined') { + throw new {{spec.title | caseUcfirst}}Exception('Chunked uploads require File and FormData globals'); + } + const [fileParam, file] = Object.entries(originalPayload).find(([_, value]) => value instanceof File) ?? []; if (!file || !fileParam) { @@ -874,7 +1069,7 @@ class Client { const response = await fetch(uri, options); // type opaque: No-CORS, different-origin response (CORS-issue) - if (response.type === 'opaque') { + if (this.runtime === 'browser' && response.type === 'opaque') { throw new {{spec.title | caseUcfirst}}Exception( `Invalid Origin. Register your new client (${window.location.host}) as a new Web platform on your project console dashboard`, 403, @@ -910,7 +1105,7 @@ class Client { const cookieFallback = response.headers.get('X-Fallback-Cookies'); - if (typeof window !== 'undefined' && window.localStorage && cookieFallback) { + if (this.runtime === 'browser' && typeof window !== 'undefined' && window.localStorage && cookieFallback) { window.console.warn('{{spec.title | caseUcfirst}} is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); window.localStorage.setItem('cookieFallback', cookieFallback); } @@ -939,7 +1134,6 @@ class Client { } export { Client, {{spec.title | caseUcfirst}}Exception }; +export type { Models, ClientAuth, AdminAuth, BrowserAuth, ServerClient, Payload, RealtimeResponseEvent, UploadProgress }; export { Query } from './query'; -export type { Models, Payload, UploadProgress }; -export type { RealtimeResponseEvent }; export type { QueryTypes, QueryTypesList } from './query'; diff --git a/templates/web/src/index.ts.twig b/templates/web/src/index.ts.twig index 5f028bd108..8e66b3b053 100644 --- a/templates/web/src/index.ts.twig +++ b/templates/web/src/index.ts.twig @@ -6,12 +6,11 @@ * [previous releases](https://github.com/{{sdk.gitUserName}}/{{sdk.gitRepoName}}/releases). */ export { Client, Query, {{spec.title | caseUcfirst}}Exception } from './client'; -export { ServerClient } from './server-client'; {% for service in spec.services %} export { {{service.name | caseUcfirst}} } from './services/{{service.name | caseKebab}}'; {% endfor %} export { Realtime } from './services/realtime'; -export type { Models, Payload, RealtimeResponseEvent, UploadProgress } from './client'; +export type { Models, Payload, RealtimeResponseEvent, UploadProgress, ClientAuth, AdminAuth, BrowserAuth, ServerClient } from './client'; export type { RealtimeSubscription } from './services/realtime'; export type { QueryTypes, QueryTypesList } from './query'; export { Permission } from './permission'; @@ -21,4 +20,4 @@ export { Channel } from './channel'; export { Operator, Condition } from './operator'; {% for enum in spec.allEnums %} export { {{ enum.name | caseUcfirst }} } from './enums/{{enum.name | caseKebab}}'; -{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/templates/web/src/server-client.ts.twig b/templates/web/src/server-client.ts.twig deleted file mode 100644 index 6cc68e7b7c..0000000000 --- a/templates/web/src/server-client.ts.twig +++ /dev/null @@ -1,291 +0,0 @@ -import { {{spec.title | caseUcfirst}}Exception, JSONbig, type Payload, type UploadProgress } from './client'; -import { Service } from './service'; - -/** - * Headers type representing a key-value pair with string keys and string values. - */ -type Headers = { - [key: string]: string; -} - -{% set serverHeaderExcludes = [ - 'session', - 'dev-key', - 'devkey', - 'impersonate-user-id', - 'impersonateuserid', - 'impersonate-user-email', - 'impersonateuseremail', - 'impersonate-user-phone', - 'impersonateuserphone' -] %} -{% set serverKeyFlag = [] %} -{% for header in spec.global.headers %} -{% if header.key|lower == 'key' %} -{% set serverKeyFlag = serverKeyFlag|merge([1]) %} -{% endif %} -{% endfor %} -{% set serverHasKey = serverKeyFlag|length > 0 %} -{% set serverFallbackKeyHeader = 'X-' ~ (spec.title | caseUcfirst) ~ '-Key' %} - -/** - * ServerClient that handles requests to {{spec.title | caseUcfirst}} from trusted server - * runtimes. Authenticates with an API key (optionally a JWT) and exposes admin-tier - * methods on the shared service classes through TypeScript type-gating. - * - * Use {@link Client} for browser/end-user contexts. ServerClient does not support - * realtime subscriptions or browser-only credentials (session, dev key, impersonation). - */ -class ServerClient { - static CHUNK_SIZE = 1024 * 1024 * 5; - - /** - * Holds configuration such as project and API key. - */ - config: { - endpoint: string; -{%~ if not serverHasKey %} - key: string; -{%~ endif %} -{%~ for header in spec.global.headers | filter(h => h.key|lower not in serverHeaderExcludes) %} - {{ header.key | caseLower }}: string; -{%~ endfor %} - } = { - endpoint: '{{ spec.endpoint }}', -{%~ if not serverHasKey %} - key: '', -{%~ endif %} -{%~ for header in spec.global.headers | filter(h => h.key|lower not in serverHeaderExcludes) %} - {{ header.key | caseLower }}: '', -{%~ endfor %} - }; - - /** - * Custom headers for API requests. - */ - headers: Headers = { - 'x-sdk-name': '{{ sdk.name }}', - 'x-sdk-platform': 'server', - 'x-sdk-language': '{{ language.name | caseLower }}', - 'x-sdk-version': '{{ sdk.version }}', - {%~ for key,header in spec.global.defaultHeaders %} - '{{key}}': '{{header}}', - {%~ endfor %} - }; - - /** - * Get Headers - * - * Returns a copy of the current request headers, including any - * authentication headers. Handle with care. - * - * @returns {Headers} - */ - getHeaders(): Headers { - return { ...this.headers }; - } - - /** - * Set Endpoint - * - * Your project endpoint - * - * @param {string} endpoint - * - * @returns {this} - */ - setEndpoint(endpoint: string): this { - if (!endpoint || typeof endpoint !== 'string') { - throw new {{spec.title | caseUcfirst}}Exception('Endpoint must be a valid string'); - } - - if (!endpoint.startsWith('http://') && !endpoint.startsWith('https://')) { - throw new {{spec.title | caseUcfirst}}Exception('Invalid endpoint URL: ' + endpoint); - } - - this.config.endpoint = endpoint; - return this; - } - - {%~ for header in spec.global.headers | filter(h => h.key|lower not in serverHeaderExcludes) %} - /** - * Set {{header.key | caseUcfirst}} - * - {%~ if header.description %} - * {{header.description}} - * - {%~ endif %} - * @param value string - * - * @return {this} - */ - set{{header.key | caseUcfirst}}(value: string): this { - this.headers['{{header.name}}'] = value; - this.config.{{ header.key | caseLower }} = value; - return this; - } - {%~ endfor %} -{%~ if not serverHasKey %} - - /** - * Set Key - * - * Your secret API key. - * - * @param value string - * - * @return {this} - */ - setKey(value: string): this { - this.headers['{{ serverFallbackKeyHeader }}'] = value; - this.config.key = value; - return this; - } -{%~ endif %} - - prepareRequest(method: string, url: URL, headers: Headers = {}, params: Payload = {}): { uri: string, options: RequestInit } { - method = method.toUpperCase(); - - headers = Object.assign({}, this.headers, headers); - - let options: RequestInit = { - method, - headers, - }; - - if (method === 'GET') { - for (const [key, value] of Object.entries(Service.flatten(params))) { - url.searchParams.append(key, value); - } - } else { - switch (headers['content-type']) { - case 'application/json': - options.body = JSONbig.stringify(params); - break; - - case 'multipart/form-data': - if (typeof FormData === 'undefined' || typeof File === 'undefined') { - throw new {{spec.title | caseUcfirst}}Exception('Multipart requests require File and FormData globals'); - } - - const formData = new FormData(); - - for (const [key, value] of Object.entries(params)) { - if (value instanceof File) { - formData.append(key, value, value.name); - } else if (Array.isArray(value)) { - for (const nestedValue of value) { - formData.append(`${key}[]`, nestedValue); - } - } else { - formData.append(key, value); - } - } - - options.body = formData; - delete headers['content-type']; - break; - } - } - - return { uri: url.toString(), options }; - } - - async chunkedUpload(method: string, url: URL, headers: Headers = {}, originalPayload: Payload = {}, onProgress: (progress: UploadProgress) => void) { - if (typeof File === 'undefined' || typeof FormData === 'undefined') { - throw new {{spec.title | caseUcfirst}}Exception('Chunked uploads require File and FormData globals'); - } - - const [fileParam, file] = Object.entries(originalPayload).find(([_, value]) => value instanceof File) ?? []; - - if (!file || !fileParam) { - throw new Error('File not found in payload'); - } - - if (file.size <= ServerClient.CHUNK_SIZE) { - return await this.call(method, url, headers, originalPayload); - } - - let start = 0; - let response = null; - - while (start < file.size) { - let end = start + ServerClient.CHUNK_SIZE; - if (end >= file.size) { - end = file.size; - } - - const chunkHeaders = { ...headers }; - chunkHeaders['content-range'] = `bytes ${start}-${end-1}/${file.size}`; - const chunk = file.slice(start, end); - - let payload = { ...originalPayload }; - payload[fileParam] = new File([chunk], file.name); - - response = await this.call(method, url, chunkHeaders, payload); - - if (onProgress && typeof onProgress === 'function') { - onProgress({ - $id: response.$id, - progress: Math.round((end / file.size) * 100), - sizeUploaded: end, - chunksTotal: Math.ceil(file.size / ServerClient.CHUNK_SIZE), - chunksUploaded: Math.ceil(end / ServerClient.CHUNK_SIZE) - }); - } - - if (response && response.$id) { - headers['x-{{spec.title | caseLower }}-id'] = response.$id; - } - - start = end; - } - - return response; - } - - async ping(): Promise { - return this.call('GET', new URL(this.config.endpoint + '/ping')); - } - - async call(method: string, url: URL, headers: Headers = {}, params: Payload = {}, responseType = 'json'): Promise { - const { uri, options } = this.prepareRequest(method, url, headers, params); - - let data: any = null; - - const response = await fetch(uri, options); - - const warnings = response.headers.get('x-{{ spec.title | lower }}-warning'); - if (warnings) { - warnings.split(';').forEach((warning: string) => console.warn('Warning: ' + warning)); - } - - if (response.headers.get('content-type')?.includes('application/json')) { - data = JSONbig.parse(await response.text()); - } else if (responseType === 'arrayBuffer') { - data = await response.arrayBuffer(); - } else { - data = { - message: await response.text() - }; - } - - if (400 <= response.status) { - let responseText = ''; - if (response.headers.get('content-type')?.includes('application/json') || responseType === 'arrayBuffer') { - responseText = JSONbig.stringify(data); - } else { - responseText = data?.message; - } - throw new {{spec.title | caseUcfirst}}Exception(data?.message, response.status, data?.type, responseText); - } - - if (data && typeof data === 'object') { - data.toString = () => JSONbig.stringify(data); - } - - return data; - } -} - -export { ServerClient }; diff --git a/templates/web/src/services/realtime.ts.twig b/templates/web/src/services/realtime.ts.twig index ec7d3cf593..36f3cb6a9e 100644 --- a/templates/web/src/services/realtime.ts.twig +++ b/templates/web/src/services/realtime.ts.twig @@ -1,4 +1,4 @@ -import { {{ spec.title | caseUcfirst}}Exception, Client, JSONbig } from '../client'; +import { {{ spec.title | caseUcfirst}}Exception, Client, JSONbig, type BrowserAuth } from '../client'; import { Channel, ActionableChannel, ResolvedChannel } from '../channel'; import { Query } from '../query'; import { ID } from '../id'; @@ -80,7 +80,7 @@ export class Realtime { private readonly DEBOUNCE_MS = 1; private readonly HEARTBEAT_INTERVAL = 20000; // 20 seconds in milliseconds - private client: Client; + private client: Client; private socket?: WebSocket; private activeSubscriptions = new Map>(); private pendingSubscribes = new Map(); @@ -95,7 +95,7 @@ export class Realtime { private onCloseCallbacks: Array<() => void> = []; private onOpenCallbacks: Array<() => void> = []; - constructor(client: Client) { + constructor(client: Client) { this.client = client; } diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index 7f8119bc63..844a0d880d 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -18,10 +18,7 @@ {% set hasMixedTier = hasClientTier and hasServerTier %} {% set hasUpload = uploadFlag|length > 0 %} import { Service } from '../service'; -import { {{ spec.title | caseUcfirst}}Exception, {% if hasClientTier %}Client, {% endif %}type Payload{% if hasUpload %}, UploadProgress{% endif %} } from '../client'; -{%~ if hasServerTier %} -import type { ServerClient } from '../server-client'; -{%~ endif %} +import { {{ spec.title | caseUcfirst}}Exception, Client, type ClientAuth, type AdminAuth, type BrowserAuth, type Payload{% if hasUpload %}, UploadProgress{% endif %} } from '../client'; import type { Models } from '../models'; {% set added = [] %} @@ -37,24 +34,24 @@ import { {{ parameter.enumName | caseUcfirst }} } from '../enums/{{ parameter.en {% endfor %} {%~ if hasMixedTier %} -export class {{ service.name | caseUcfirst }} { - client: TClient; +export class {{ service.name | caseUcfirst }} { + client: Client; - constructor(client: TClient) { + constructor(client: Client) { this.client = client; } {%~ elseif hasServerTier %} export class {{ service.name | caseUcfirst }} { - client: ServerClient; + client: Client; - constructor(client: ServerClient) { + constructor(client: Client) { this.client = client; } {%~ else %} export class {{ service.name | caseUcfirst }} { - client: Client; + client: Client; - constructor(client: Client) { + constructor(client: Client) { this.client = client; } {%~ endif %} @@ -65,13 +62,13 @@ export class {{ service.name | caseUcfirst }} { {%~ set methodSupportsServer = 'server' in method.platforms or 'console' in method.platforms %} {%~ if hasMixedTier %} {%~ if (method.type == 'location' or method.type == 'webAuth') and not methodSupportsClient %} - {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} + {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} {%~ elseif method.type == 'location' or method.type == 'webAuth' %} - {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} + {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} {%~ elseif not methodSupportsClient %} - {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} + {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} {%~ elseif not methodSupportsServer %} - {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} + {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} {%~ endif %} {%~ endif %} /** From d1823d756095ba5e03013a3d0b1cc8352f7c2432 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 30 Apr 2026 16:22:42 +0530 Subject: [PATCH 04/69] Address web client review feedback --- templates/web/src/client.ts.twig | 31 ++++++++----------------------- templates/web/src/index.ts.twig | 2 +- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 6ce17354d4..1828dad455 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -348,7 +348,6 @@ type ClientRuntime = 'browser' | 'server'; type ClientAuth = 'anonymous' | 'session' | 'jwt' | 'apiKey' | 'devKey' | 'impersonation'; type AdminAuth = 'apiKey' | 'devKey'; type BrowserAuth = 'anonymous' | 'session' | 'jwt' | 'impersonation'; -type ServerClient = Client; type BaseClientParams = { endpoint: string; @@ -419,7 +418,6 @@ class Client { }; private runtime: ClientRuntime = '{{ sdk.platform == 'server' ? 'server' : 'browser' }}'; - private auth: ClientAuth = 'anonymous'; private readonly authType?: TAuth; /** @@ -436,7 +434,7 @@ class Client { }; static anonymous(params: BaseClientParams): Client<'anonymous'> { - return new Client<'anonymous'>().applyBase(params, 'anonymous', 'browser'); + return new Client<'anonymous'>().applyBase<'anonymous'>(params, 'browser'); } static fromAnonymous(params: BaseClientParams): Client<'anonymous'> { @@ -445,13 +443,13 @@ class Client { static fromSession(params: SessionClientParams): Client<'session'> { return new Client<'session'>() - .applyBase(params, 'session', 'browser') + .applyBase<'session'>(params, 'browser') .setSession(params.session); } static fromApiKey(params: ApiKeyClientParams): Client<'apiKey'> { const client = new Client<'apiKey'>() - .applyBase(params, 'apiKey', 'server') + .applyBase<'apiKey'>(params, 'server') .setKey(params.apiKey); if (params.jwt !== undefined) { @@ -467,13 +465,13 @@ class Client { static fromJWT(params: JWTClientParams): Client<'jwt'> { return new Client<'jwt'>() - .applyBase(params, 'jwt', 'browser') + .applyBase<'jwt'>(params, 'browser') .setJWT(params.jwt); } static fromDevKey(params: DevKeyClientParams): Client<'devKey'> { return new Client<'devKey'>() - .applyBase(params, 'devKey', 'server') + .applyBase<'devKey'>(params, 'server') .setDevKey(params.devKey); } @@ -489,7 +487,7 @@ class Client { } const client = new Client<'impersonation'>() - .applyBase(params, 'impersonation', 'browser') + .applyBase<'impersonation'>(params, 'browser') .setSession(params.session); if (params.userId !== undefined) { @@ -501,9 +499,8 @@ class Client { return client.setImpersonateUserPhone(params.phone); } - private applyBase(params: BaseClientParams, auth: T, runtime: ClientRuntime): Client { + private applyBase(params: BaseClientParams, runtime: ClientRuntime): Client { const client = this as unknown as Client; - client.auth = auth; client.runtime = runtime; client.headers['x-sdk-platform'] = runtime === 'server' ? 'server' : 'client'; client.setEndpoint(params.endpoint); @@ -597,9 +594,6 @@ class Client { setKey(value: string): Client<'apiKey'> { this.headers['X-{{ spec.title | caseUcfirst }}-Key'] = value; this.config.key = value; - this.auth = 'apiKey'; - this.runtime = 'server'; - this.headers['x-sdk-platform'] = 'server'; return this as unknown as Client<'apiKey'>; } @@ -618,18 +612,12 @@ class Client { setSession(value: string): Client<'session'> { this.headers['X-{{ spec.title | caseUcfirst }}-Session'] = value; this.config.session = value; - this.auth = 'session'; - this.runtime = 'browser'; - this.headers['x-sdk-platform'] = 'client'; return this as unknown as Client<'session'>; } setDevKey(value: string): Client<'devKey'> { this.headers['X-{{ spec.title | caseUcfirst }}-Dev-Key'] = value; this.config.devkey = value; - this.auth = 'devKey'; - this.runtime = 'server'; - this.headers['x-sdk-platform'] = 'server'; return this as unknown as Client<'devKey'>; } @@ -642,21 +630,18 @@ class Client { setImpersonateUserId(value: string): Client<'impersonation'> { this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id'] = value; this.config.impersonateuserid = value; - this.auth = 'impersonation'; return this as unknown as Client<'impersonation'>; } setImpersonateUserEmail(value: string): Client<'impersonation'> { this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email'] = value; this.config.impersonateuseremail = value; - this.auth = 'impersonation'; return this as unknown as Client<'impersonation'>; } setImpersonateUserPhone(value: string): Client<'impersonation'> { this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone'] = value; this.config.impersonateuserphone = value; - this.auth = 'impersonation'; return this as unknown as Client<'impersonation'>; } @@ -1134,6 +1119,6 @@ class Client { } export { Client, {{spec.title | caseUcfirst}}Exception }; -export type { Models, ClientAuth, AdminAuth, BrowserAuth, ServerClient, Payload, RealtimeResponseEvent, UploadProgress }; +export type { Models, ClientAuth, AdminAuth, BrowserAuth, Payload, RealtimeResponseEvent, UploadProgress }; export { Query } from './query'; export type { QueryTypes, QueryTypesList } from './query'; diff --git a/templates/web/src/index.ts.twig b/templates/web/src/index.ts.twig index 8e66b3b053..d2843eac9c 100644 --- a/templates/web/src/index.ts.twig +++ b/templates/web/src/index.ts.twig @@ -10,7 +10,7 @@ export { Client, Query, {{spec.title | caseUcfirst}}Exception } from './client'; export { {{service.name | caseUcfirst}} } from './services/{{service.name | caseKebab}}'; {% endfor %} export { Realtime } from './services/realtime'; -export type { Models, Payload, RealtimeResponseEvent, UploadProgress, ClientAuth, AdminAuth, BrowserAuth, ServerClient } from './client'; +export type { Models, Payload, RealtimeResponseEvent, UploadProgress, ClientAuth, AdminAuth, BrowserAuth } from './client'; export type { RealtimeSubscription } from './services/realtime'; export type { QueryTypes, QueryTypesList } from './query'; export { Permission } from './permission'; From 32058bf058c69e3c1df2e611a4b2596d7ad1ae4a Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 30 Apr 2026 16:30:02 +0530 Subject: [PATCH 05/69] Fix web client setter runtime behavior --- templates/web/src/client.ts.twig | 51 +++++++++++++-------- templates/web/src/services/realtime.ts.twig | 8 +++- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 1828dad455..e5afb7fc4f 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -517,6 +517,15 @@ class Client { return client; } + private withRuntime(runtime: ClientRuntime): Client { + const client = new Client(); + client.config = { ...this.config }; + client.headers = { ...this.headers }; + client.runtime = runtime; + client.headers['x-sdk-platform'] = runtime === 'server' ? 'server' : 'client'; + return client; + } + /** * Get Headers * @@ -592,9 +601,10 @@ class Client { } setKey(value: string): Client<'apiKey'> { - this.headers['X-{{ spec.title | caseUcfirst }}-Key'] = value; - this.config.key = value; - return this as unknown as Client<'apiKey'>; + const client = this.withRuntime<'apiKey'>('server'); + client.headers['X-{{ spec.title | caseUcfirst }}-Key'] = value; + client.config.key = value; + return client; } setJWT(value: string): this { @@ -610,15 +620,17 @@ class Client { } setSession(value: string): Client<'session'> { - this.headers['X-{{ spec.title | caseUcfirst }}-Session'] = value; - this.config.session = value; - return this as unknown as Client<'session'>; + const client = this.withRuntime<'session'>('browser'); + client.headers['X-{{ spec.title | caseUcfirst }}-Session'] = value; + client.config.session = value; + return client; } setDevKey(value: string): Client<'devKey'> { - this.headers['X-{{ spec.title | caseUcfirst }}-Dev-Key'] = value; - this.config.devkey = value; - return this as unknown as Client<'devKey'>; + const client = this.withRuntime<'devKey'>('server'); + client.headers['X-{{ spec.title | caseUcfirst }}-Dev-Key'] = value; + client.config.devkey = value; + return client; } setForwardedUserAgent(value: string): this { @@ -628,21 +640,24 @@ class Client { } setImpersonateUserId(value: string): Client<'impersonation'> { - this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id'] = value; - this.config.impersonateuserid = value; - return this as unknown as Client<'impersonation'>; + const client = this.withRuntime<'impersonation'>('browser'); + client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id'] = value; + client.config.impersonateuserid = value; + return client; } setImpersonateUserEmail(value: string): Client<'impersonation'> { - this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email'] = value; - this.config.impersonateuseremail = value; - return this as unknown as Client<'impersonation'>; + const client = this.withRuntime<'impersonation'>('browser'); + client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email'] = value; + client.config.impersonateuseremail = value; + return client; } setImpersonateUserPhone(value: string): Client<'impersonation'> { - this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone'] = value; - this.config.impersonateuserphone = value; - return this as unknown as Client<'impersonation'>; + const client = this.withRuntime<'impersonation'>('browser'); + client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone'] = value; + client.config.impersonateuserphone = value; + return client; } private realtime: Realtime = { diff --git a/templates/web/src/services/realtime.ts.twig b/templates/web/src/services/realtime.ts.twig index 36f3cb6a9e..5edd84eb9d 100644 --- a/templates/web/src/services/realtime.ts.twig +++ b/templates/web/src/services/realtime.ts.twig @@ -1,4 +1,8 @@ +{% if language.name == 'Web' %} import { {{ spec.title | caseUcfirst}}Exception, Client, JSONbig, type BrowserAuth } from '../client'; +{% else %} +import { {{ spec.title | caseUcfirst}}Exception, Client, JSONbig } from '../client'; +{% endif %} import { Channel, ActionableChannel, ResolvedChannel } from '../channel'; import { Query } from '../query'; import { ID } from '../id'; @@ -80,7 +84,7 @@ export class Realtime { private readonly DEBOUNCE_MS = 1; private readonly HEARTBEAT_INTERVAL = 20000; // 20 seconds in milliseconds - private client: Client; + private client: Client{% if language.name == 'Web' %}{% endif %}; private socket?: WebSocket; private activeSubscriptions = new Map>(); private pendingSubscribes = new Map(); @@ -95,7 +99,7 @@ export class Realtime { private onCloseCallbacks: Array<() => void> = []; private onOpenCallbacks: Array<() => void> = []; - constructor(client: Client) { + constructor(client: Client{% if language.name == 'Web' %}{% endif %}) { this.client = client; } From c71be4a60977ff9c1bfce69b362c82055b12f60a Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 30 Apr 2026 16:42:45 +0530 Subject: [PATCH 06/69] Preserve fluent setter mutation behavior --- templates/web/src/client.ts.twig | 63 +++++++++++++++----------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index e5afb7fc4f..bd93ba3f84 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -517,15 +517,6 @@ class Client { return client; } - private withRuntime(runtime: ClientRuntime): Client { - const client = new Client(); - client.config = { ...this.config }; - client.headers = { ...this.headers }; - client.runtime = runtime; - client.headers['x-sdk-platform'] = runtime === 'server' ? 'server' : 'client'; - return client; - } - /** * Get Headers * @@ -601,10 +592,11 @@ class Client { } setKey(value: string): Client<'apiKey'> { - const client = this.withRuntime<'apiKey'>('server'); - client.headers['X-{{ spec.title | caseUcfirst }}-Key'] = value; - client.config.key = value; - return client; + this.headers['X-{{ spec.title | caseUcfirst }}-Key'] = value; + this.config.key = value; + this.runtime = 'server'; + this.headers['x-sdk-platform'] = 'server'; + return this as unknown as Client<'apiKey'>; } setJWT(value: string): this { @@ -620,17 +612,19 @@ class Client { } setSession(value: string): Client<'session'> { - const client = this.withRuntime<'session'>('browser'); - client.headers['X-{{ spec.title | caseUcfirst }}-Session'] = value; - client.config.session = value; - return client; + this.headers['X-{{ spec.title | caseUcfirst }}-Session'] = value; + this.config.session = value; + this.runtime = 'browser'; + this.headers['x-sdk-platform'] = 'client'; + return this as unknown as Client<'session'>; } setDevKey(value: string): Client<'devKey'> { - const client = this.withRuntime<'devKey'>('server'); - client.headers['X-{{ spec.title | caseUcfirst }}-Dev-Key'] = value; - client.config.devkey = value; - return client; + this.headers['X-{{ spec.title | caseUcfirst }}-Dev-Key'] = value; + this.config.devkey = value; + this.runtime = 'server'; + this.headers['x-sdk-platform'] = 'server'; + return this as unknown as Client<'devKey'>; } setForwardedUserAgent(value: string): this { @@ -640,24 +634,27 @@ class Client { } setImpersonateUserId(value: string): Client<'impersonation'> { - const client = this.withRuntime<'impersonation'>('browser'); - client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id'] = value; - client.config.impersonateuserid = value; - return client; + this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id'] = value; + this.config.impersonateuserid = value; + this.runtime = 'browser'; + this.headers['x-sdk-platform'] = 'client'; + return this as unknown as Client<'impersonation'>; } setImpersonateUserEmail(value: string): Client<'impersonation'> { - const client = this.withRuntime<'impersonation'>('browser'); - client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email'] = value; - client.config.impersonateuseremail = value; - return client; + this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email'] = value; + this.config.impersonateuseremail = value; + this.runtime = 'browser'; + this.headers['x-sdk-platform'] = 'client'; + return this as unknown as Client<'impersonation'>; } setImpersonateUserPhone(value: string): Client<'impersonation'> { - const client = this.withRuntime<'impersonation'>('browser'); - client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone'] = value; - client.config.impersonateuserphone = value; - return client; + this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone'] = value; + this.config.impersonateuserphone = value; + this.runtime = 'browser'; + this.headers['x-sdk-platform'] = 'client'; + return this as unknown as Client<'impersonation'>; } private realtime: Realtime = { From 7b644b08ba59b48b025f24d8787cc1b5671b9857 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 30 Apr 2026 16:51:29 +0530 Subject: [PATCH 07/69] Treat dev keys as browser auth --- templates/web/src/client.ts.twig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index bd93ba3f84..12876dbe2b 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -346,8 +346,8 @@ class {{spec.title | caseUcfirst}}Exception extends Error { */ type ClientRuntime = 'browser' | 'server'; type ClientAuth = 'anonymous' | 'session' | 'jwt' | 'apiKey' | 'devKey' | 'impersonation'; -type AdminAuth = 'apiKey' | 'devKey'; -type BrowserAuth = 'anonymous' | 'session' | 'jwt' | 'impersonation'; +type AdminAuth = 'apiKey'; +type BrowserAuth = 'anonymous' | 'session' | 'jwt' | 'devKey' | 'impersonation'; type BaseClientParams = { endpoint: string; @@ -471,7 +471,7 @@ class Client { static fromDevKey(params: DevKeyClientParams): Client<'devKey'> { return new Client<'devKey'>() - .applyBase<'devKey'>(params, 'server') + .applyBase<'devKey'>(params, 'browser') .setDevKey(params.devKey); } @@ -622,8 +622,8 @@ class Client { setDevKey(value: string): Client<'devKey'> { this.headers['X-{{ spec.title | caseUcfirst }}-Dev-Key'] = value; this.config.devkey = value; - this.runtime = 'server'; - this.headers['x-sdk-platform'] = 'server'; + this.runtime = 'browser'; + this.headers['x-sdk-platform'] = 'client'; return this as unknown as Client<'devKey'>; } From a4bd8ffc37c608bc18df5e5446b9a8ae73621d6f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 30 Apr 2026 16:53:25 +0530 Subject: [PATCH 08/69] Avoid unused web auth type imports --- templates/web/src/services/template.ts.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index 844a0d880d..b492844952 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -18,7 +18,7 @@ {% set hasMixedTier = hasClientTier and hasServerTier %} {% set hasUpload = uploadFlag|length > 0 %} import { Service } from '../service'; -import { {{ spec.title | caseUcfirst}}Exception, Client, type ClientAuth, type AdminAuth, type BrowserAuth, type Payload{% if hasUpload %}, UploadProgress{% endif %} } from '../client'; +import { {{ spec.title | caseUcfirst}}Exception, Client, {% if hasMixedTier %}type ClientAuth, {% endif %}{% if hasServerTier %}type AdminAuth, {% endif %}{% if hasClientTier %}type BrowserAuth, {% endif %}type Payload{% if hasUpload %}, UploadProgress{% endif %} } from '../client'; import type { Models } from '../models'; {% set added = [] %} From 72253957dfb43d3e92f603bcbbb837b5b4cc181d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 30 Apr 2026 17:05:40 +0530 Subject: [PATCH 09/69] Allow dual-platform web auth methods --- templates/web/src/services/template.ts.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index b492844952..b907bedc08 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -63,7 +63,7 @@ export class {{ service.name | caseUcfirst }} { {%~ if hasMixedTier %} {%~ if (method.type == 'location' or method.type == 'webAuth') and not methodSupportsClient %} {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} - {%~ elseif method.type == 'location' or method.type == 'webAuth' %} + {%~ elseif (method.type == 'location' or method.type == 'webAuth') and not methodSupportsServer %} {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} {%~ elseif not methodSupportsClient %} {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} From 071a493108cb2fe789d736a84a5bd77bafaedad0 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 30 Apr 2026 17:13:05 +0530 Subject: [PATCH 10/69] Avoid unused auth imports in dual services --- templates/web/src/services/template.ts.twig | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index b907bedc08..284ce1ebfe 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -1,6 +1,8 @@ {# Detect service shape and imports before emitting TypeScript. #} {% set tierFlag = [] %} {% set serverFlag = [] %} +{% set adminAuthFlag = [] %} +{% set browserAuthFlag = [] %} {% set uploadFlag = [] %} {% for m in service.methods %} {% if 'client' in m.platforms %} @@ -9,6 +11,12 @@ {% if 'server' in m.platforms or 'console' in m.platforms %} {% set serverFlag = serverFlag|merge([1]) %} {% endif %} +{% if ('server' in m.platforms or 'console' in m.platforms) and 'client' not in m.platforms %} +{% set adminAuthFlag = adminAuthFlag|merge([1]) %} +{% endif %} +{% if 'client' in m.platforms and 'server' not in m.platforms and 'console' not in m.platforms %} +{% set browserAuthFlag = browserAuthFlag|merge([1]) %} +{% endif %} {% if 'multipart/form-data' in m.consumes %} {% set uploadFlag = uploadFlag|merge([1]) %} {% endif %} @@ -16,9 +24,11 @@ {% set hasClientTier = tierFlag|length > 0 %} {% set hasServerTier = serverFlag|length > 0 %} {% set hasMixedTier = hasClientTier and hasServerTier %} +{% set needsAdminAuth = hasServerTier and (not hasMixedTier or adminAuthFlag|length > 0) %} +{% set needsBrowserAuth = hasClientTier and (not hasMixedTier or browserAuthFlag|length > 0) %} {% set hasUpload = uploadFlag|length > 0 %} import { Service } from '../service'; -import { {{ spec.title | caseUcfirst}}Exception, Client, {% if hasMixedTier %}type ClientAuth, {% endif %}{% if hasServerTier %}type AdminAuth, {% endif %}{% if hasClientTier %}type BrowserAuth, {% endif %}type Payload{% if hasUpload %}, UploadProgress{% endif %} } from '../client'; +import { {{ spec.title | caseUcfirst}}Exception, Client, {% if hasMixedTier %}type ClientAuth, {% endif %}{% if needsAdminAuth %}type AdminAuth, {% endif %}{% if needsBrowserAuth %}type BrowserAuth, {% endif %}type Payload{% if hasUpload %}, UploadProgress{% endif %} } from '../client'; import type { Models } from '../models'; {% set added = [] %} From dfc32b2956e6328c6844a381436329bb11f0ec26 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 4 May 2026 14:26:52 +0530 Subject: [PATCH 11/69] Restore console auth factory options --- .github/workflows/sdk-build-validation.yml | 5 +- templates/web/src/client.ts.twig | 58 ++++++++++++++++++++-- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/.github/workflows/sdk-build-validation.yml b/.github/workflows/sdk-build-validation.yml index 28637ac25b..a0cad3c856 100644 --- a/.github/workflows/sdk-build-validation.yml +++ b/.github/workflows/sdk-build-validation.yml @@ -21,9 +21,6 @@ jobs: matrix: include: # Client SDKs - - sdk: web - platform: client - - sdk: flutter platform: client @@ -73,7 +70,7 @@ jobs: # Console SDKs - sdk: cli platform: console - + - sdk: web platform: console diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 12876dbe2b..181c518d75 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -345,8 +345,8 @@ class {{spec.title | caseUcfirst}}Exception extends Error { * Client that handles requests to {{spec.title | caseUcfirst}} */ type ClientRuntime = 'browser' | 'server'; -type ClientAuth = 'anonymous' | 'session' | 'jwt' | 'apiKey' | 'devKey' | 'impersonation'; -type AdminAuth = 'apiKey'; +type ClientAuth = 'anonymous' | 'session' | 'jwt' | 'apiKey'{% if sdk.platform == 'console' %} | 'cookie'{% endif %} | 'devKey' | 'impersonation'; +type AdminAuth = 'apiKey'{% if sdk.platform == 'console' %} | 'cookie'{% endif %}; type BrowserAuth = 'anonymous' | 'session' | 'jwt' | 'devKey' | 'impersonation'; type BaseClientParams = { @@ -354,6 +354,10 @@ type BaseClientParams = { projectId: string; endpointRealtime?: string; locale?: string; +{%~ if sdk.platform == 'console' %} + mode?: string; + platform?: string; +{%~ endif %} }; type SessionClientParams = BaseClientParams & { @@ -366,6 +370,12 @@ type ApiKeyClientParams = BaseClientParams & { forwardedUserAgent?: string; }; +{%~ if sdk.platform == 'console' %} +type CookieClientParams = BaseClientParams & { + cookie: string; +}; + +{%~ endif %} type JWTClientParams = BaseClientParams & { jwt: string; }; @@ -392,6 +402,11 @@ class Client { endpointRealtime: string; project: string; key: string; +{%~ if sdk.platform == 'console' %} + cookie: string; + mode: string; + platform: string; +{%~ endif %} jwt: string; locale: string; session: string; @@ -406,6 +421,11 @@ class Client { endpointRealtime: '', project: '', key: '', +{%~ if sdk.platform == 'console' %} + cookie: '', + mode: '', + platform: '', +{%~ endif %} jwt: '', locale: '', session: '', @@ -463,6 +483,14 @@ class Client { return client; } +{%~ if sdk.platform == 'console' %} + static fromCookie(params: CookieClientParams): Client<'cookie'> { + return new Client<'cookie'>() + .applyBase<'cookie'>(params, 'server') + .setCookie(params.cookie); + } + +{%~ endif %} static fromJWT(params: JWTClientParams): Client<'jwt'> { return new Client<'jwt'>() .applyBase<'jwt'>(params, 'browser') @@ -502,7 +530,7 @@ class Client { private applyBase(params: BaseClientParams, runtime: ClientRuntime): Client { const client = this as unknown as Client; client.runtime = runtime; - client.headers['x-sdk-platform'] = runtime === 'server' ? 'server' : 'client'; + client.headers['x-sdk-platform'] = runtime === 'server' ? '{{ sdk.platform == 'console' ? 'console' : 'server' }}' : 'client'; client.setEndpoint(params.endpoint); client.setProject(params.projectId); @@ -514,6 +542,18 @@ class Client { client.setLocale(params.locale); } +{%~ if sdk.platform == 'console' %} + if (params.mode !== undefined) { + client.headers['X-{{ spec.title | caseUcfirst }}-Mode'] = params.mode; + client.config.mode = params.mode; + } + + if (params.platform !== undefined) { + client.headers['X-{{ spec.title | caseUcfirst }}-Platform'] = params.platform; + client.config.platform = params.platform; + } + +{%~ endif %} return client; } @@ -595,10 +635,20 @@ class Client { this.headers['X-{{ spec.title | caseUcfirst }}-Key'] = value; this.config.key = value; this.runtime = 'server'; - this.headers['x-sdk-platform'] = 'server'; + this.headers['x-sdk-platform'] = '{{ sdk.platform == 'console' ? 'console' : 'server' }}'; return this as unknown as Client<'apiKey'>; } +{%~ if sdk.platform == 'console' %} + setCookie(value: string): Client<'cookie'> { + this.headers['Cookie'] = value; + this.config.cookie = value; + this.runtime = 'server'; + this.headers['x-sdk-platform'] = '{{ sdk.platform == 'console' ? 'console' : 'server' }}'; + return this as unknown as Client<'cookie'>; + } + +{%~ endif %} setJWT(value: string): this { this.headers['X-{{ spec.title | caseUcfirst }}-JWT'] = value; this.config.jwt = value; From bfbfb171760467d0d89ea536bae10fc20bd995c8 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 4 May 2026 14:32:44 +0530 Subject: [PATCH 12/69] Rename web auth type aliases by platform --- templates/web/src/client.ts.twig | 13 ++++---- templates/web/src/index.ts.twig | 2 +- templates/web/src/services/realtime.ts.twig | 6 ++-- templates/web/src/services/template.ts.twig | 33 +++++++++++---------- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 181c518d75..e1935e4174 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -345,9 +345,10 @@ class {{spec.title | caseUcfirst}}Exception extends Error { * Client that handles requests to {{spec.title | caseUcfirst}} */ type ClientRuntime = 'browser' | 'server'; -type ClientAuth = 'anonymous' | 'session' | 'jwt' | 'apiKey'{% if sdk.platform == 'console' %} | 'cookie'{% endif %} | 'devKey' | 'impersonation'; -type AdminAuth = 'apiKey'{% if sdk.platform == 'console' %} | 'cookie'{% endif %}; -type BrowserAuth = 'anonymous' | 'session' | 'jwt' | 'devKey' | 'impersonation'; +type ClientAuth = 'anonymous' | 'session' | 'jwt' | 'devKey' | 'impersonation'; +type ServerAuth = 'apiKey'; +type ConsoleAuth = {% if sdk.platform == 'console' %}'apiKey' | 'cookie'{% else %}never{% endif %}; +type Auth = ClientAuth | ServerAuth | ConsoleAuth; type BaseClientParams = { endpoint: string; @@ -391,7 +392,7 @@ type ImpersonationTarget = type ImpersonationClientParams = SessionClientParams & ImpersonationTarget; -class Client { +class Client { static CHUNK_SIZE = 1024 * 1024 * 5; /** @@ -527,7 +528,7 @@ class Client { return client.setImpersonateUserPhone(params.phone); } - private applyBase(params: BaseClientParams, runtime: ClientRuntime): Client { + private applyBase(params: BaseClientParams, runtime: ClientRuntime): Client { const client = this as unknown as Client; client.runtime = runtime; client.headers['x-sdk-platform'] = runtime === 'server' ? '{{ sdk.platform == 'console' ? 'console' : 'server' }}' : 'client'; @@ -1181,6 +1182,6 @@ class Client { } export { Client, {{spec.title | caseUcfirst}}Exception }; -export type { Models, ClientAuth, AdminAuth, BrowserAuth, Payload, RealtimeResponseEvent, UploadProgress }; +export type { Models, ClientAuth, ServerAuth, ConsoleAuth, Payload, RealtimeResponseEvent, UploadProgress }; export { Query } from './query'; export type { QueryTypes, QueryTypesList } from './query'; diff --git a/templates/web/src/index.ts.twig b/templates/web/src/index.ts.twig index d2843eac9c..8a8a978584 100644 --- a/templates/web/src/index.ts.twig +++ b/templates/web/src/index.ts.twig @@ -10,7 +10,7 @@ export { Client, Query, {{spec.title | caseUcfirst}}Exception } from './client'; export { {{service.name | caseUcfirst}} } from './services/{{service.name | caseKebab}}'; {% endfor %} export { Realtime } from './services/realtime'; -export type { Models, Payload, RealtimeResponseEvent, UploadProgress, ClientAuth, AdminAuth, BrowserAuth } from './client'; +export type { Models, Payload, RealtimeResponseEvent, UploadProgress, ClientAuth, ServerAuth, ConsoleAuth } from './client'; export type { RealtimeSubscription } from './services/realtime'; export type { QueryTypes, QueryTypesList } from './query'; export { Permission } from './permission'; diff --git a/templates/web/src/services/realtime.ts.twig b/templates/web/src/services/realtime.ts.twig index 5edd84eb9d..d3d9ca82d0 100644 --- a/templates/web/src/services/realtime.ts.twig +++ b/templates/web/src/services/realtime.ts.twig @@ -1,5 +1,5 @@ {% if language.name == 'Web' %} -import { {{ spec.title | caseUcfirst}}Exception, Client, JSONbig, type BrowserAuth } from '../client'; +import { {{ spec.title | caseUcfirst}}Exception, Client, JSONbig, type ClientAuth } from '../client'; {% else %} import { {{ spec.title | caseUcfirst}}Exception, Client, JSONbig } from '../client'; {% endif %} @@ -84,7 +84,7 @@ export class Realtime { private readonly DEBOUNCE_MS = 1; private readonly HEARTBEAT_INTERVAL = 20000; // 20 seconds in milliseconds - private client: Client{% if language.name == 'Web' %}{% endif %}; + private client: Client{% if language.name == 'Web' %}{% endif %}; private socket?: WebSocket; private activeSubscriptions = new Map>(); private pendingSubscribes = new Map(); @@ -99,7 +99,7 @@ export class Realtime { private onCloseCallbacks: Array<() => void> = []; private onOpenCallbacks: Array<() => void> = []; - constructor(client: Client{% if language.name == 'Web' %}{% endif %}) { + constructor(client: Client{% if language.name == 'Web' %}{% endif %}) { this.client = client; } diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index 284ce1ebfe..195719f32c 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -1,8 +1,8 @@ {# Detect service shape and imports before emitting TypeScript. #} {% set tierFlag = [] %} {% set serverFlag = [] %} -{% set adminAuthFlag = [] %} -{% set browserAuthFlag = [] %} +{% set serverAuthFlag = [] %} +{% set clientAuthFlag = [] %} {% set uploadFlag = [] %} {% for m in service.methods %} {% if 'client' in m.platforms %} @@ -12,10 +12,10 @@ {% set serverFlag = serverFlag|merge([1]) %} {% endif %} {% if ('server' in m.platforms or 'console' in m.platforms) and 'client' not in m.platforms %} -{% set adminAuthFlag = adminAuthFlag|merge([1]) %} +{% set serverAuthFlag = serverAuthFlag|merge([1]) %} {% endif %} {% if 'client' in m.platforms and 'server' not in m.platforms and 'console' not in m.platforms %} -{% set browserAuthFlag = browserAuthFlag|merge([1]) %} +{% set clientAuthFlag = clientAuthFlag|merge([1]) %} {% endif %} {% if 'multipart/form-data' in m.consumes %} {% set uploadFlag = uploadFlag|merge([1]) %} @@ -24,11 +24,12 @@ {% set hasClientTier = tierFlag|length > 0 %} {% set hasServerTier = serverFlag|length > 0 %} {% set hasMixedTier = hasClientTier and hasServerTier %} -{% set needsAdminAuth = hasServerTier and (not hasMixedTier or adminAuthFlag|length > 0) %} -{% set needsBrowserAuth = hasClientTier and (not hasMixedTier or browserAuthFlag|length > 0) %} +{% set platformAuth = sdk.platform == 'console' ? 'ConsoleAuth' : 'ServerAuth' %} +{% set needsPlatformAuth = hasServerTier and (not hasMixedTier or serverAuthFlag|length > 0) %} +{% set needsClientAuth = hasClientTier and (not hasMixedTier or clientAuthFlag|length > 0) %} {% set hasUpload = uploadFlag|length > 0 %} import { Service } from '../service'; -import { {{ spec.title | caseUcfirst}}Exception, Client, {% if hasMixedTier %}type ClientAuth, {% endif %}{% if needsAdminAuth %}type AdminAuth, {% endif %}{% if needsBrowserAuth %}type BrowserAuth, {% endif %}type Payload{% if hasUpload %}, UploadProgress{% endif %} } from '../client'; +import { {{ spec.title | caseUcfirst}}Exception, Client, {% if needsClientAuth or hasMixedTier %}type ClientAuth, {% endif %}{% if needsPlatformAuth or hasMixedTier %}type {{ platformAuth }}, {% endif %}type Payload{% if hasUpload %}, UploadProgress{% endif %} } from '../client'; import type { Models } from '../models'; {% set added = [] %} @@ -44,7 +45,7 @@ import { {{ parameter.enumName | caseUcfirst }} } from '../enums/{{ parameter.en {% endfor %} {%~ if hasMixedTier %} -export class {{ service.name | caseUcfirst }} { +export class {{ service.name | caseUcfirst }} { client: Client; constructor(client: Client) { @@ -52,16 +53,16 @@ export class {{ service.name | caseUcfirst }}; + client: Client<{{ platformAuth }}>; - constructor(client: Client) { + constructor(client: Client<{{ platformAuth }}>) { this.client = client; } {%~ else %} export class {{ service.name | caseUcfirst }} { - client: Client; + client: Client; - constructor(client: Client) { + constructor(client: Client) { this.client = client; } {%~ endif %} @@ -72,13 +73,13 @@ export class {{ service.name | caseUcfirst }} { {%~ set methodSupportsServer = 'server' in method.platforms or 'console' in method.platforms %} {%~ if hasMixedTier %} {%~ if (method.type == 'location' or method.type == 'webAuth') and not methodSupportsClient %} - {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} + {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ '<' ~ platformAuth ~ '>, ' %} {%~ elseif (method.type == 'location' or method.type == 'webAuth') and not methodSupportsServer %} - {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} + {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} {%~ elseif not methodSupportsClient %} - {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} + {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ '<' ~ platformAuth ~ '>, ' %} {%~ elseif not methodSupportsServer %} - {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} + {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} {%~ endif %} {%~ endif %} /** From e70155ecfd4fa8fccc740b0f4cc3467b34f23933 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 4 May 2026 14:38:15 +0530 Subject: [PATCH 13/69] Remove client-platform web runtime fallback --- templates/web/src/client.ts.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index e1935e4174..e1afb7a92c 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -438,7 +438,7 @@ class Client { selfSigned: false, }; - private runtime: ClientRuntime = '{{ sdk.platform == 'server' ? 'server' : 'browser' }}'; + private runtime: ClientRuntime = '{{ sdk.platform == 'console' ? 'browser' : 'server' }}'; private readonly authType?: TAuth; /** From 65137feae588aed1782c30da846f095766d3cff0 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 4 May 2026 14:40:35 +0530 Subject: [PATCH 14/69] Clean examples before generation --- example.php | 77 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 22 deletions(-) diff --git a/example.php b/example.php index 01a7cb8140..9caafe15ef 100644 --- a/example.php +++ b/example.php @@ -103,6 +103,39 @@ function configureSDK($sdk, $overrides = []) { return $sdk; } + function cleanupDirectory(string $target): void { + if (!is_dir($target)) { + return; + } + + $examplesRoot = realpath(__DIR__ . '/examples'); + $targetPath = realpath($target); + + if ($examplesRoot === false || $targetPath === false || !str_starts_with($targetPath, $examplesRoot . DIRECTORY_SEPARATOR)) { + throw new Exception('Refusing to clean directory outside examples: ' . $target); + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($targetPath, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($iterator as $file) { + if ($file->isDir()) { + rmdir($file->getPathname()); + } else { + unlink($file->getPathname()); + } + } + + rmdir($targetPath); + } + + function generateExample(SDK $sdk, string $target): void { + cleanupDirectory($target); + $sdk->generate($target); + } + $requestedSdk = isset($argv[1]) ? $argv[1] : null; $requestedPlatform = isset($argv[2]) ? $argv[2] : null; @@ -138,21 +171,21 @@ function configureSDK($sdk, $overrides = []) { ->setComposerPackage('appwrite'); $sdk = new SDK($php, new Swagger2($spec)); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/php'); + generateExample($sdk, __DIR__ . '/examples/php'); } // Web if (!$requestedSdk || $requestedSdk === 'web') { $sdk = new SDK(new Web(), new Swagger2($spec)); configureSDK($sdk, ['platform' => $platform]); - $sdk->generate(__DIR__ . '/examples/web'); + generateExample($sdk, __DIR__ . '/examples/web'); } // Node if (!$requestedSdk || $requestedSdk === 'node') { $sdk = new SDK(new Node(), new Swagger2($spec)); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/node'); + generateExample($sdk, __DIR__ . '/examples/node'); } // CLI @@ -188,21 +221,21 @@ function configureSDK($sdk, $overrides = []) { ], ]); - $sdk->generate(__DIR__ . '/examples/cli'); + generateExample($sdk, __DIR__ . '/examples/cli'); } // Ruby if (!$requestedSdk || $requestedSdk === 'ruby') { $sdk = new SDK(new Ruby(), new Swagger2($spec)); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/ruby'); + generateExample($sdk, __DIR__ . '/examples/ruby'); } // Python if (!$requestedSdk || $requestedSdk === 'python') { $sdk = new SDK(new Python(), new Swagger2($spec)); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/python'); + generateExample($sdk, __DIR__ . '/examples/python'); } // Dart @@ -211,7 +244,7 @@ function configureSDK($sdk, $overrides = []) { $dart->setPackageName('dart_appwrite'); $sdk = new SDK($dart, new Swagger2($spec)); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/dart'); + generateExample($sdk, __DIR__ . '/examples/dart'); } // Flutter @@ -220,7 +253,7 @@ function configureSDK($sdk, $overrides = []) { $flutter->setPackageName('appwrite'); $sdk = new SDK($flutter, new Swagger2($spec)); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/flutter'); + generateExample($sdk, __DIR__ . '/examples/flutter'); } // React Native @@ -229,42 +262,42 @@ function configureSDK($sdk, $overrides = []) { $reactNative->setNPMPackage('react-native-appwrite'); $sdk = new SDK($reactNative, new Swagger2($spec)); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/react-native'); + generateExample($sdk, __DIR__ . '/examples/react-native'); } // GO if (!$requestedSdk || $requestedSdk === 'go') { $sdk = new SDK(new Go(), new Swagger2($spec)); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/go'); + generateExample($sdk, __DIR__ . '/examples/go'); } // Swift if (!$requestedSdk || $requestedSdk === 'swift') { $sdk = new SDK(new Swift(), new Swagger2($spec)); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/swift'); + generateExample($sdk, __DIR__ . '/examples/swift'); } // Apple if (!$requestedSdk || $requestedSdk === 'apple') { $sdk = new SDK(new Apple(), new Swagger2($spec)); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/apple'); + generateExample($sdk, __DIR__ . '/examples/apple'); } // DotNet if (!$requestedSdk || $requestedSdk === 'dotnet') { $sdk = new SDK(new DotNet(), new Swagger2($spec)); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/dotnet'); + generateExample($sdk, __DIR__ . '/examples/dotnet'); } // REST if (!$requestedSdk || $requestedSdk === 'rest') { $sdk = new SDK(new REST(), new Swagger2($spec)); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/REST'); + generateExample($sdk, __DIR__ . '/examples/REST'); } // Android @@ -273,7 +306,7 @@ function configureSDK($sdk, $overrides = []) { configureSDK($sdk, [ 'namespace' => 'io.appwrite', ]); - $sdk->generate(__DIR__ . '/examples/android'); + generateExample($sdk, __DIR__ . '/examples/android'); } // Kotlin @@ -282,14 +315,14 @@ function configureSDK($sdk, $overrides = []) { configureSDK($sdk, [ 'namespace' => 'io.appwrite', ]); - $sdk->generate(__DIR__ . '/examples/kotlin'); + generateExample($sdk, __DIR__ . '/examples/kotlin'); } // GraphQL if (!$requestedSdk || $requestedSdk === 'graphql') { $sdk = new SDK(new GraphQL(), new Swagger2($spec)); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/graphql'); + generateExample($sdk, __DIR__ . '/examples/graphql'); } // Markdown @@ -298,7 +331,7 @@ function configureSDK($sdk, $overrides = []) { $markdown->setNPMPackage('@appwrite.io/docs'); $sdk = new SDK($markdown, new Swagger2($spec)); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/markdown'); + generateExample($sdk, __DIR__ . '/examples/markdown'); } // Agent Skills if (!$requestedSdk || $requestedSdk === 'agent-skills') { @@ -310,7 +343,7 @@ function configureSDK($sdk, $overrides = []) { licenseURL: 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE', )); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/agent-skills'); + generateExample($sdk, __DIR__ . '/examples/agent-skills'); } // Cursor Plugin @@ -323,7 +356,7 @@ function configureSDK($sdk, $overrides = []) { licenseURL: 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE', )); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/cursor-plugin'); + generateExample($sdk, __DIR__ . '/examples/cursor-plugin'); } // Claude Plugin @@ -336,14 +369,14 @@ function configureSDK($sdk, $overrides = []) { licenseURL: 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE', )); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/claude-plugin'); + generateExample($sdk, __DIR__ . '/examples/claude-plugin'); } // Rust if (!$requestedSdk || $requestedSdk === 'rust') { $sdk = new SDK(new Rust(), new Swagger2($spec)); configureSDK($sdk); - $sdk->generate(__DIR__ . '/examples/rust'); + generateExample($sdk, __DIR__ . '/examples/rust'); } } catch (Exception $exception) { From 3eba0b287fec1ae3a161bf64374e13ab7eb95b9b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 4 May 2026 14:50:00 +0530 Subject: [PATCH 15/69] Rename browser client factory --- templates/web/src/client.ts.twig | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index e1afb7a92c..efac9084f8 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -345,7 +345,7 @@ class {{spec.title | caseUcfirst}}Exception extends Error { * Client that handles requests to {{spec.title | caseUcfirst}} */ type ClientRuntime = 'browser' | 'server'; -type ClientAuth = 'anonymous' | 'session' | 'jwt' | 'devKey' | 'impersonation'; +type ClientAuth = 'browser' | 'session' | 'jwt' | 'devKey' | 'impersonation'; type ServerAuth = 'apiKey'; type ConsoleAuth = {% if sdk.platform == 'console' %}'apiKey' | 'cookie'{% else %}never{% endif %}; type Auth = ClientAuth | ServerAuth | ConsoleAuth; @@ -392,7 +392,7 @@ type ImpersonationTarget = type ImpersonationClientParams = SessionClientParams & ImpersonationTarget; -class Client { +class Client { static CHUNK_SIZE = 1024 * 1024 * 5; /** @@ -454,12 +454,8 @@ class Client { {%~ endfor %} }; - static anonymous(params: BaseClientParams): Client<'anonymous'> { - return new Client<'anonymous'>().applyBase<'anonymous'>(params, 'browser'); - } - - static fromAnonymous(params: BaseClientParams): Client<'anonymous'> { - return Client.anonymous(params); + static fromBrowser(params: BaseClientParams): Client<'browser'> { + return new Client<'browser'>().applyBase<'browser'>(params, 'browser'); } static fromSession(params: SessionClientParams): Client<'session'> { From f699aefa49bb2bd4e9be0c80155a82de585bf833 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 4 May 2026 14:55:45 +0530 Subject: [PATCH 16/69] Fix web client runtime default --- templates/web/src/client.ts.twig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index efac9084f8..5185a5b4a9 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -438,8 +438,7 @@ class Client { selfSigned: false, }; - private runtime: ClientRuntime = '{{ sdk.platform == 'console' ? 'browser' : 'server' }}'; - private readonly authType?: TAuth; + private runtime: ClientRuntime = '{{ sdk.platform == 'server' ? 'server' : 'browser' }}'; /** * Custom headers for API requests. From 000b946304ee28c6511c71438cc5f82191ebe764 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 4 May 2026 14:57:47 +0530 Subject: [PATCH 17/69] Treat web JWT auth as server auth --- templates/web/src/client.ts.twig | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 5185a5b4a9..f8d4d6449a 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -345,8 +345,8 @@ class {{spec.title | caseUcfirst}}Exception extends Error { * Client that handles requests to {{spec.title | caseUcfirst}} */ type ClientRuntime = 'browser' | 'server'; -type ClientAuth = 'browser' | 'session' | 'jwt' | 'devKey' | 'impersonation'; -type ServerAuth = 'apiKey'; +type ClientAuth = 'browser' | 'session' | 'devKey' | 'impersonation'; +type ServerAuth = 'apiKey' | 'jwt'; type ConsoleAuth = {% if sdk.platform == 'console' %}'apiKey' | 'cookie'{% else %}never{% endif %}; type Auth = ClientAuth | ServerAuth | ConsoleAuth; @@ -489,7 +489,7 @@ class Client { {%~ endif %} static fromJWT(params: JWTClientParams): Client<'jwt'> { return new Client<'jwt'>() - .applyBase<'jwt'>(params, 'browser') + .applyBase<'jwt'>(params, 'server') .setJWT(params.jwt); } @@ -645,10 +645,12 @@ class Client { } {%~ endif %} - setJWT(value: string): this { + setJWT(value: string): Client<'jwt'> { this.headers['X-{{ spec.title | caseUcfirst }}-JWT'] = value; this.config.jwt = value; - return this; + this.runtime = 'server'; + this.headers['x-sdk-platform'] = '{{ sdk.platform == 'console' ? 'console' : 'server' }}'; + return this as unknown as Client<'jwt'>; } setLocale(value: string): this { From 8dded007c787fd7f3a903381014a7de645b8a82b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 4 May 2026 15:44:04 +0530 Subject: [PATCH 18/69] refactor(web): address review feedback on typed client - Rename fromApiKey -> fromAPIKey for naming consistency - Make all setters private; expose only static factory methods - Guard window access with typeof window !== 'undefined' in realtime - Gate fromAPIKey behind server/console platform builds only - Normalize ClientRuntime to 'client' | 'server'; remove 'browser' - Add withJWT and withForwardedUserAgent builder methods - Fix clearTimeout misuse on interval handles --- templates/web/src/client.ts.twig | 129 +++++++++++--------- templates/web/src/services/realtime.ts.twig | 28 +++-- 2 files changed, 94 insertions(+), 63 deletions(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index f8d4d6449a..dd3bdb2f4a 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -344,7 +344,7 @@ class {{spec.title | caseUcfirst}}Exception extends Error { /** * Client that handles requests to {{spec.title | caseUcfirst}} */ -type ClientRuntime = 'browser' | 'server'; +type ClientRuntime = 'client' | 'server'; type ClientAuth = 'browser' | 'session' | 'devKey' | 'impersonation'; type ServerAuth = 'apiKey' | 'jwt'; type ConsoleAuth = {% if sdk.platform == 'console' %}'apiKey' | 'cookie'{% else %}never{% endif %}; @@ -367,8 +367,6 @@ type SessionClientParams = BaseClientParams & { type ApiKeyClientParams = BaseClientParams & { apiKey: string; - jwt?: string; - forwardedUserAgent?: string; }; {%~ if sdk.platform == 'console' %} @@ -438,7 +436,7 @@ class Client { selfSigned: false, }; - private runtime: ClientRuntime = '{{ sdk.platform == 'server' ? 'server' : 'browser' }}'; + private runtime: ClientRuntime = '{{ sdk.platform == 'server' ? 'server' : 'client' }}'; /** * Custom headers for API requests. @@ -454,31 +452,24 @@ class Client { }; static fromBrowser(params: BaseClientParams): Client<'browser'> { - return new Client<'browser'>().applyBase<'browser'>(params, 'browser'); + return new Client<'browser'>().applyBase<'browser'>(params, 'client'); } static fromSession(params: SessionClientParams): Client<'session'> { return new Client<'session'>() - .applyBase<'session'>(params, 'browser') + .applyBase<'session'>(params, 'client') .setSession(params.session); } - static fromApiKey(params: ApiKeyClientParams): Client<'apiKey'> { - const client = new Client<'apiKey'>() +{%~ if sdk.platform == 'server' or sdk.platform == 'console' %} + static fromAPIKey(params: ApiKeyClientParams): Client<'apiKey'> { + return new Client<'apiKey'>() .applyBase<'apiKey'>(params, 'server') .setKey(params.apiKey); - - if (params.jwt !== undefined) { - client.setJWT(params.jwt); - } - - if (params.forwardedUserAgent !== undefined) { - client.setForwardedUserAgent(params.forwardedUserAgent); - } - - return client; } +{%~ endif %} + {%~ if sdk.platform == 'console' %} static fromCookie(params: CookieClientParams): Client<'cookie'> { return new Client<'cookie'>() @@ -495,7 +486,7 @@ class Client { static fromDevKey(params: DevKeyClientParams): Client<'devKey'> { return new Client<'devKey'>() - .applyBase<'devKey'>(params, 'browser') + .applyBase<'devKey'>(params, 'client') .setDevKey(params.devKey); } @@ -511,7 +502,7 @@ class Client { } const client = new Client<'impersonation'>() - .applyBase<'impersonation'>(params, 'browser') + .applyBase<'impersonation'>(params, 'client') .setSession(params.session); if (params.userId !== undefined) { @@ -523,6 +514,18 @@ class Client { return client.setImpersonateUserPhone(params.phone); } + withJWT(jwt: string): this { + this.headers['X-{{ spec.title | caseUcfirst }}-JWT'] = jwt; + this.config.jwt = jwt; + return this; + } + + withForwardedUserAgent(forwardedUserAgent: string): this { + this.headers['X-Forwarded-User-Agent'] = forwardedUserAgent; + this.config.forwardeduseragent = forwardedUserAgent; + return this; + } + private applyBase(params: BaseClientParams, runtime: ClientRuntime): Client { const client = this as unknown as Client; client.runtime = runtime; @@ -574,7 +577,7 @@ class Client { * * @returns {this} */ - setEndpoint(endpoint: string): this { + private setEndpoint(endpoint: string): this { if (!endpoint || typeof endpoint !== 'string') { throw new {{spec.title | caseUcfirst}}Exception('Endpoint must be a valid string'); } @@ -596,7 +599,7 @@ class Client { * * @returns {this} */ - setEndpointRealtime(endpointRealtime: string): this { + private setEndpointRealtime(endpointRealtime: string): this { if (!endpointRealtime || typeof endpointRealtime !== 'string') { throw new {{spec.title | caseUcfirst}}Exception('Endpoint must be a valid string'); } @@ -616,18 +619,18 @@ class Client { * * @returns {this} */ - setSelfSigned(selfSigned: boolean): this { + private setSelfSigned(selfSigned: boolean): this { this.config.selfSigned = selfSigned; return this; } - setProject(value: string): this { + private setProject(value: string): this { this.headers['X-{{ spec.title | caseUcfirst }}-Project'] = value; this.config.project = value; return this; } - setKey(value: string): Client<'apiKey'> { + private setKey(value: string): Client<'apiKey'> { this.headers['X-{{ spec.title | caseUcfirst }}-Key'] = value; this.config.key = value; this.runtime = 'server'; @@ -636,7 +639,7 @@ class Client { } {%~ if sdk.platform == 'console' %} - setCookie(value: string): Client<'cookie'> { + private setCookie(value: string): Client<'cookie'> { this.headers['Cookie'] = value; this.config.cookie = value; this.runtime = 'server'; @@ -645,7 +648,7 @@ class Client { } {%~ endif %} - setJWT(value: string): Client<'jwt'> { + private setJWT(value: string): Client<'jwt'> { this.headers['X-{{ spec.title | caseUcfirst }}-JWT'] = value; this.config.jwt = value; this.runtime = 'server'; @@ -653,54 +656,54 @@ class Client { return this as unknown as Client<'jwt'>; } - setLocale(value: string): this { + private setLocale(value: string): this { this.headers['X-{{ spec.title | caseUcfirst }}-Locale'] = value; this.config.locale = value; return this; } - setSession(value: string): Client<'session'> { + private setSession(value: string): Client<'session'> { this.headers['X-{{ spec.title | caseUcfirst }}-Session'] = value; this.config.session = value; - this.runtime = 'browser'; + this.runtime = 'client'; this.headers['x-sdk-platform'] = 'client'; return this as unknown as Client<'session'>; } - setDevKey(value: string): Client<'devKey'> { + private setDevKey(value: string): Client<'devKey'> { this.headers['X-{{ spec.title | caseUcfirst }}-Dev-Key'] = value; this.config.devkey = value; - this.runtime = 'browser'; + this.runtime = 'client'; this.headers['x-sdk-platform'] = 'client'; return this as unknown as Client<'devKey'>; } - setForwardedUserAgent(value: string): this { + private setForwardedUserAgent(value: string): this { this.headers['X-Forwarded-User-Agent'] = value; this.config.forwardeduseragent = value; return this; } - setImpersonateUserId(value: string): Client<'impersonation'> { + private setImpersonateUserId(value: string): Client<'impersonation'> { this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id'] = value; this.config.impersonateuserid = value; - this.runtime = 'browser'; + this.runtime = 'client'; this.headers['x-sdk-platform'] = 'client'; return this as unknown as Client<'impersonation'>; } - setImpersonateUserEmail(value: string): Client<'impersonation'> { + private setImpersonateUserEmail(value: string): Client<'impersonation'> { this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email'] = value; this.config.impersonateuseremail = value; - this.runtime = 'browser'; + this.runtime = 'client'; this.headers['x-sdk-platform'] = 'client'; return this as unknown as Client<'impersonation'>; } - setImpersonateUserPhone(value: string): Client<'impersonation'> { + private setImpersonateUserPhone(value: string): Client<'impersonation'> { this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone'] = value; this.config.impersonateuserphone = value; - this.runtime = 'browser'; + this.runtime = 'client'; this.headers['x-sdk-platform'] = 'client'; return this as unknown as Client<'impersonation'>; } @@ -718,9 +721,13 @@ class Client { lastMessage: undefined, connect: () => { clearTimeout(this.realtime.timeout); - this.realtime.timeout = window?.setTimeout(() => { - this.realtime.createSocket(); - }, 50); + this.realtime.timeout = typeof window !== 'undefined' + ? window.setTimeout(() => { + this.realtime.createSocket(); + }, 50) + : setTimeout(() => { + this.realtime.createSocket(); + }, 50) as unknown as TimeoutHandle; }, getTimeout: () => { switch (true) { @@ -736,14 +743,20 @@ class Client { }, createHeartbeat: () => { if (this.realtime.heartbeat) { - clearTimeout(this.realtime.heartbeat); + clearInterval(this.realtime.heartbeat as any); } - this.realtime.heartbeat = window?.setInterval(() => { - this.realtime.socket?.send(JSONbig.stringify({ - type: 'ping' - })); - }, 20_000); + this.realtime.heartbeat = typeof window !== 'undefined' + ? window.setInterval(() => { + this.realtime.socket?.send(JSONbig.stringify({ + type: 'ping' + })); + }, 20_000) + : setInterval(() => { + this.realtime.socket?.send(JSONbig.stringify({ + type: 'ping' + })); + }, 20_000) as unknown as TimeoutHandle; }, createSocket: () => { if (this.realtime.subscriptions.size < 1) { @@ -829,8 +842,14 @@ class Client { let session = this.config.session; if (!session) { - const cookie = JSONbig.parse(window.localStorage.getItem('cookieFallback') ?? '{}'); - session = cookie?.[`a_session_${this.config.project}`]; + try { + if (typeof window !== 'undefined' && window.localStorage) { + const cookie = JSONbig.parse(window.localStorage.getItem('cookieFallback') ?? '{}'); + session = cookie?.[`a_session_${this.config.project}`]; + } + } catch (error) { + console.error('Failed to parse cookie fallback:', error); + } } if (session && !messageData?.user) { this.realtime.socket?.send(JSONbig.stringify({ @@ -995,7 +1014,7 @@ class Client { headers = Object.assign({}, this.headers, headers); - if (this.runtime === 'browser' && typeof window !== 'undefined' && window.localStorage) { + if (this.runtime === 'client' && typeof window !== 'undefined' && window.localStorage) { const cookieFallback = window.localStorage.getItem('cookieFallback'); if (cookieFallback) { headers['X-Fallback-Cookies'] = cookieFallback; @@ -1007,7 +1026,7 @@ class Client { headers, }; - if (this.runtime === 'browser' && headers['X-Appwrite-Dev-Key'] === undefined) { + if (this.runtime === 'client' && headers['X-Appwrite-Dev-Key'] === undefined) { options.credentials = 'include'; } @@ -1114,9 +1133,9 @@ class Client { const response = await fetch(uri, options); // type opaque: No-CORS, different-origin response (CORS-issue) - if (this.runtime === 'browser' && response.type === 'opaque') { + if (this.runtime === 'client' && typeof window !== 'undefined' && response.type === 'opaque') { throw new {{spec.title | caseUcfirst}}Exception( - `Invalid Origin. Register your new client (${window.location.host}) as a new Web platform on your project console dashboard`, + `Invalid Origin. Register your new client (${typeof window !== 'undefined' ? window.location.host : 'unknown'}) as a new Web platform on your project console dashboard`, 403, "forbidden", "" @@ -1150,7 +1169,7 @@ class Client { const cookieFallback = response.headers.get('X-Fallback-Cookies'); - if (this.runtime === 'browser' && typeof window !== 'undefined' && window.localStorage && cookieFallback) { + if (this.runtime === 'client' && typeof window !== 'undefined' && window.localStorage && cookieFallback) { window.console.warn('{{spec.title | caseUcfirst}} is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); window.localStorage.setItem('cookieFallback', cookieFallback); } diff --git a/templates/web/src/services/realtime.ts.twig b/templates/web/src/services/realtime.ts.twig index d3d9ca82d0..18c66f132d 100644 --- a/templates/web/src/services/realtime.ts.twig +++ b/templates/web/src/services/realtime.ts.twig @@ -135,16 +135,26 @@ export class Realtime { private startHeartbeat(): void { this.stopHeartbeat(); - this.heartbeatTimer = window?.setInterval(() => { - if (this.socket && this.socket.readyState === WebSocket.OPEN) { - this.socket.send(JSONbig.stringify({ type: 'ping' })); - } - }, this.HEARTBEAT_INTERVAL); + this.heartbeatTimer = typeof window !== 'undefined' + ? window.setInterval(() => { + if (this.socket && this.socket.readyState === WebSocket.OPEN) { + this.socket.send(JSONbig.stringify({ type: 'ping' })); + } + }, this.HEARTBEAT_INTERVAL) + : setInterval(() => { + if (this.socket && this.socket.readyState === WebSocket.OPEN) { + this.socket.send(JSONbig.stringify({ type: 'ping' })); + } + }, this.HEARTBEAT_INTERVAL) as unknown as number; } private stopHeartbeat(): void { if (this.heartbeatTimer) { - window?.clearInterval(this.heartbeatTimer); + if (typeof window !== 'undefined') { + window.clearInterval(this.heartbeatTimer); + } else { + clearInterval(this.heartbeatTimer as any); + } this.heartbeatTimer = undefined; } } @@ -582,8 +592,10 @@ export class Realtime { let session = this.client.config.session; if (!session) { try { - const cookie = JSONbig.parse(window.localStorage.getItem('cookieFallback') ?? '{}'); - session = cookie?.[`a_session_${this.client.config.project}`]; + if (typeof window !== 'undefined' && window.localStorage) { + const cookie = JSONbig.parse(window.localStorage.getItem('cookieFallback') ?? '{}'); + session = cookie?.[`a_session_${this.client.config.project}`]; + } } catch (error) { console.error('Failed to parse cookie fallback:', error); } From 51446b5921f632a5e45708dd9135c60248322b62 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 4 May 2026 15:50:30 +0530 Subject: [PATCH 19/69] formatting --- .github/workflows/sdk-build-validation.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sdk-build-validation.yml b/.github/workflows/sdk-build-validation.yml index a0cad3c856..28637ac25b 100644 --- a/.github/workflows/sdk-build-validation.yml +++ b/.github/workflows/sdk-build-validation.yml @@ -21,6 +21,9 @@ jobs: matrix: include: # Client SDKs + - sdk: web + platform: client + - sdk: flutter platform: client @@ -70,7 +73,7 @@ jobs: # Console SDKs - sdk: cli platform: console - + - sdk: web platform: console From 1dc15b26396455cd24b8398b4516e8ca8116ce38 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 4 May 2026 16:00:04 +0530 Subject: [PATCH 20/69] refactor(web): move auth detection and this-gate logic into Twig filters Moves the service-level auth tier detection and per-method this-gate construction from template.ts.twig set blocks into PHP helpers exposed as Twig filters (webServiceAuth, webMethodThisGate). This makes the template easier to read while keeping generated output identical. --- src/SDK/Language/Web.php | 105 ++++++++++++++++++++ templates/web/src/services/template.ts.twig | 57 ++--------- 2 files changed, 113 insertions(+), 49 deletions(-) diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index 28002b7c09..a86494333a 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -476,6 +476,105 @@ public function getSubSchema(array $property, array $spec, string $methodName = return $this->getTypeName($property); } + /** + * Determine the TypeScript auth type name for a given platform. + */ + protected function webPlatformAuth(string $platform): string + { + return $platform === 'console' ? 'ConsoleAuth' : 'ServerAuth'; + } + + /** + * 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 methodSupportsPlatformAuth(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 + */ + public function webServiceAuth(array $service, string $platform): array + { + $hasClientTier = false; + $hasServerTier = false; + $hasServerOnly = false; + $hasClientOnly = false; + $hasUpload = false; + + foreach ($service['methods'] ?? [] as $method) { + $platforms = $method['platforms'] ?? []; + $hasClient = in_array('client', $platforms, true); + $hasServerOrConsole = in_array('server', $platforms, true) || in_array('console', $platforms, true); + + if ($hasClient) { + $hasClientTier = true; + } + if ($hasServerOrConsole) { + $hasServerTier = true; + } + if ($hasServerOrConsole && !$hasClient) { + $hasServerOnly = true; + } + if ($hasClient && !$hasServerOrConsole) { + $hasClientOnly = true; + } + if (in_array('multipart/form-data', $method['consumes'] ?? [], true)) { + $hasUpload = true; + } + } + + $hasMixedTier = $hasClientTier && $hasServerTier; + $platformAuth = $this->webPlatformAuth($platform); + + return [ + 'hasClientTier' => $hasClientTier, + 'hasServerTier' => $hasServerTier, + 'hasMixedTier' => $hasMixedTier, + 'platformAuth' => $platformAuth, + 'needsPlatformAuth' => $hasServerTier && (!$hasMixedTier || $hasServerOnly), + 'needsClientAuth' => $hasClientTier && (!$hasMixedTier || $hasClientOnly), + 'hasUpload' => $hasUpload, + ]; + } + + /** + * Build the TypeScript `this:` gate string for a method in a Web service. + */ + public function webMethodThisGate(array $method, array $service, string $platform): string + { + $auth = $this->webServiceAuth($service, $platform); + if (!$auth['hasMixedTier']) { + return ''; + } + + $methodSupportsClient = $this->methodSupportsClient($method); + $methodSupportsPlatform = $this->methodSupportsPlatformAuth($method); + + $serviceName = $this->toPascalCase($service['name'] ?? ''); + + if (!$methodSupportsClient) { + return 'this: ' . $serviceName . '<' . $auth['platformAuth'] . '>, '; + } + if (!$methodSupportsPlatform) { + return 'this: ' . $serviceName . ', '; + } + + return ''; + } + public function getFilters(): array { return \array_merge(parent::getFilters(), [ @@ -536,6 +635,12 @@ public function getFilters(): array return $condition; }, ['is_safe' => ['html']]), + new TwigFilter('webServiceAuth', function (array $service, string $platform) { + return $this->webServiceAuth($service, $platform); + }), + new TwigFilter('webMethodThisGate', function (array $method, array $service, string $platform) { + return $this->webMethodThisGate($method, $service, $platform); + }, ['is_safe' => ['html']]), new TwigFilter('comment2', function ($value) { $value = explode("\n", $value); foreach ($value as $key => $line) { diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index 195719f32c..da884f78be 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -1,35 +1,7 @@ {# Detect service shape and imports before emitting TypeScript. #} -{% set tierFlag = [] %} -{% set serverFlag = [] %} -{% set serverAuthFlag = [] %} -{% set clientAuthFlag = [] %} -{% set uploadFlag = [] %} -{% for m in service.methods %} -{% if 'client' in m.platforms %} -{% set tierFlag = tierFlag|merge([1]) %} -{% endif %} -{% if 'server' in m.platforms or 'console' in m.platforms %} -{% set serverFlag = serverFlag|merge([1]) %} -{% endif %} -{% if ('server' in m.platforms or 'console' in m.platforms) and 'client' not in m.platforms %} -{% set serverAuthFlag = serverAuthFlag|merge([1]) %} -{% endif %} -{% if 'client' in m.platforms and 'server' not in m.platforms and 'console' not in m.platforms %} -{% set clientAuthFlag = clientAuthFlag|merge([1]) %} -{% endif %} -{% if 'multipart/form-data' in m.consumes %} -{% set uploadFlag = uploadFlag|merge([1]) %} -{% endif %} -{% endfor %} -{% set hasClientTier = tierFlag|length > 0 %} -{% set hasServerTier = serverFlag|length > 0 %} -{% set hasMixedTier = hasClientTier and hasServerTier %} -{% set platformAuth = sdk.platform == 'console' ? 'ConsoleAuth' : 'ServerAuth' %} -{% set needsPlatformAuth = hasServerTier and (not hasMixedTier or serverAuthFlag|length > 0) %} -{% set needsClientAuth = hasClientTier and (not hasMixedTier or clientAuthFlag|length > 0) %} -{% set hasUpload = uploadFlag|length > 0 %} +{% set auth = service | webServiceAuth(sdk.platform) %} import { Service } from '../service'; -import { {{ spec.title | caseUcfirst}}Exception, Client, {% if needsClientAuth or hasMixedTier %}type ClientAuth, {% endif %}{% if needsPlatformAuth or hasMixedTier %}type {{ platformAuth }}, {% endif %}type Payload{% if hasUpload %}, UploadProgress{% endif %} } from '../client'; +import { {{ spec.title | caseUcfirst}}Exception, Client, {% if auth.needsClientAuth or auth.hasMixedTier %}type ClientAuth, {% endif %}{% if auth.needsPlatformAuth or auth.hasMixedTier %}type {{ auth.platformAuth }}, {% endif %}type Payload{% if auth.hasUpload %}, UploadProgress{% endif %} } from '../client'; import type { Models } from '../models'; {% set added = [] %} @@ -44,18 +16,18 @@ import { {{ parameter.enumName | caseUcfirst }} } from '../enums/{{ parameter.en {% endfor %} {% endfor %} -{%~ if hasMixedTier %} -export class {{ service.name | caseUcfirst }} { +{%~ if auth.hasMixedTier %} +export class {{ service.name | caseUcfirst }} { client: Client; constructor(client: Client) { this.client = client; } -{%~ elseif hasServerTier %} +{%~ elseif auth.hasServerTier %} export class {{ service.name | caseUcfirst }} { - client: Client<{{ platformAuth }}>; + client: Client<{{ auth.platformAuth }}>; - constructor(client: Client<{{ platformAuth }}>) { + constructor(client: Client<{{ auth.platformAuth }}>) { this.client = client; } {%~ else %} @@ -68,20 +40,7 @@ export class {{ service.name | caseUcfirst }} { {%~ endif %} {%~ for method in service.methods %} - {%~ set thisGate = '' %} - {%~ set methodSupportsClient = 'client' in method.platforms %} - {%~ set methodSupportsServer = 'server' in method.platforms or 'console' in method.platforms %} - {%~ if hasMixedTier %} - {%~ if (method.type == 'location' or method.type == 'webAuth') and not methodSupportsClient %} - {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ '<' ~ platformAuth ~ '>, ' %} - {%~ elseif (method.type == 'location' or method.type == 'webAuth') and not methodSupportsServer %} - {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} - {%~ elseif not methodSupportsClient %} - {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ '<' ~ platformAuth ~ '>, ' %} - {%~ elseif not methodSupportsServer %} - {%~ set thisGate = 'this: ' ~ (service.name | caseUcfirst) ~ ', ' %} - {%~ endif %} - {%~ endif %} + {%~ set thisGate = method | webMethodThisGate(service, sdk.platform) %} /** {%~ if method.description %} * {{ method.description | replace({'\n': '\n * '}) | raw }} From 2b735138c6090bda917aaee8e52a10f706c7e2db Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 4 May 2026 16:05:26 +0530 Subject: [PATCH 21/69] formatting --- example.php | 77 +++++++++++++++-------------------------------------- 1 file changed, 22 insertions(+), 55 deletions(-) diff --git a/example.php b/example.php index 9caafe15ef..01a7cb8140 100644 --- a/example.php +++ b/example.php @@ -103,39 +103,6 @@ function configureSDK($sdk, $overrides = []) { return $sdk; } - function cleanupDirectory(string $target): void { - if (!is_dir($target)) { - return; - } - - $examplesRoot = realpath(__DIR__ . '/examples'); - $targetPath = realpath($target); - - if ($examplesRoot === false || $targetPath === false || !str_starts_with($targetPath, $examplesRoot . DIRECTORY_SEPARATOR)) { - throw new Exception('Refusing to clean directory outside examples: ' . $target); - } - - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($targetPath, FilesystemIterator::SKIP_DOTS), - RecursiveIteratorIterator::CHILD_FIRST - ); - - foreach ($iterator as $file) { - if ($file->isDir()) { - rmdir($file->getPathname()); - } else { - unlink($file->getPathname()); - } - } - - rmdir($targetPath); - } - - function generateExample(SDK $sdk, string $target): void { - cleanupDirectory($target); - $sdk->generate($target); - } - $requestedSdk = isset($argv[1]) ? $argv[1] : null; $requestedPlatform = isset($argv[2]) ? $argv[2] : null; @@ -171,21 +138,21 @@ function generateExample(SDK $sdk, string $target): void { ->setComposerPackage('appwrite'); $sdk = new SDK($php, new Swagger2($spec)); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/php'); + $sdk->generate(__DIR__ . '/examples/php'); } // Web if (!$requestedSdk || $requestedSdk === 'web') { $sdk = new SDK(new Web(), new Swagger2($spec)); configureSDK($sdk, ['platform' => $platform]); - generateExample($sdk, __DIR__ . '/examples/web'); + $sdk->generate(__DIR__ . '/examples/web'); } // Node if (!$requestedSdk || $requestedSdk === 'node') { $sdk = new SDK(new Node(), new Swagger2($spec)); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/node'); + $sdk->generate(__DIR__ . '/examples/node'); } // CLI @@ -221,21 +188,21 @@ function generateExample(SDK $sdk, string $target): void { ], ]); - generateExample($sdk, __DIR__ . '/examples/cli'); + $sdk->generate(__DIR__ . '/examples/cli'); } // Ruby if (!$requestedSdk || $requestedSdk === 'ruby') { $sdk = new SDK(new Ruby(), new Swagger2($spec)); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/ruby'); + $sdk->generate(__DIR__ . '/examples/ruby'); } // Python if (!$requestedSdk || $requestedSdk === 'python') { $sdk = new SDK(new Python(), new Swagger2($spec)); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/python'); + $sdk->generate(__DIR__ . '/examples/python'); } // Dart @@ -244,7 +211,7 @@ function generateExample(SDK $sdk, string $target): void { $dart->setPackageName('dart_appwrite'); $sdk = new SDK($dart, new Swagger2($spec)); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/dart'); + $sdk->generate(__DIR__ . '/examples/dart'); } // Flutter @@ -253,7 +220,7 @@ function generateExample(SDK $sdk, string $target): void { $flutter->setPackageName('appwrite'); $sdk = new SDK($flutter, new Swagger2($spec)); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/flutter'); + $sdk->generate(__DIR__ . '/examples/flutter'); } // React Native @@ -262,42 +229,42 @@ function generateExample(SDK $sdk, string $target): void { $reactNative->setNPMPackage('react-native-appwrite'); $sdk = new SDK($reactNative, new Swagger2($spec)); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/react-native'); + $sdk->generate(__DIR__ . '/examples/react-native'); } // GO if (!$requestedSdk || $requestedSdk === 'go') { $sdk = new SDK(new Go(), new Swagger2($spec)); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/go'); + $sdk->generate(__DIR__ . '/examples/go'); } // Swift if (!$requestedSdk || $requestedSdk === 'swift') { $sdk = new SDK(new Swift(), new Swagger2($spec)); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/swift'); + $sdk->generate(__DIR__ . '/examples/swift'); } // Apple if (!$requestedSdk || $requestedSdk === 'apple') { $sdk = new SDK(new Apple(), new Swagger2($spec)); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/apple'); + $sdk->generate(__DIR__ . '/examples/apple'); } // DotNet if (!$requestedSdk || $requestedSdk === 'dotnet') { $sdk = new SDK(new DotNet(), new Swagger2($spec)); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/dotnet'); + $sdk->generate(__DIR__ . '/examples/dotnet'); } // REST if (!$requestedSdk || $requestedSdk === 'rest') { $sdk = new SDK(new REST(), new Swagger2($spec)); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/REST'); + $sdk->generate(__DIR__ . '/examples/REST'); } // Android @@ -306,7 +273,7 @@ function generateExample(SDK $sdk, string $target): void { configureSDK($sdk, [ 'namespace' => 'io.appwrite', ]); - generateExample($sdk, __DIR__ . '/examples/android'); + $sdk->generate(__DIR__ . '/examples/android'); } // Kotlin @@ -315,14 +282,14 @@ function generateExample(SDK $sdk, string $target): void { configureSDK($sdk, [ 'namespace' => 'io.appwrite', ]); - generateExample($sdk, __DIR__ . '/examples/kotlin'); + $sdk->generate(__DIR__ . '/examples/kotlin'); } // GraphQL if (!$requestedSdk || $requestedSdk === 'graphql') { $sdk = new SDK(new GraphQL(), new Swagger2($spec)); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/graphql'); + $sdk->generate(__DIR__ . '/examples/graphql'); } // Markdown @@ -331,7 +298,7 @@ function generateExample(SDK $sdk, string $target): void { $markdown->setNPMPackage('@appwrite.io/docs'); $sdk = new SDK($markdown, new Swagger2($spec)); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/markdown'); + $sdk->generate(__DIR__ . '/examples/markdown'); } // Agent Skills if (!$requestedSdk || $requestedSdk === 'agent-skills') { @@ -343,7 +310,7 @@ function generateExample(SDK $sdk, string $target): void { licenseURL: 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE', )); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/agent-skills'); + $sdk->generate(__DIR__ . '/examples/agent-skills'); } // Cursor Plugin @@ -356,7 +323,7 @@ function generateExample(SDK $sdk, string $target): void { licenseURL: 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE', )); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/cursor-plugin'); + $sdk->generate(__DIR__ . '/examples/cursor-plugin'); } // Claude Plugin @@ -369,14 +336,14 @@ function generateExample(SDK $sdk, string $target): void { licenseURL: 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE', )); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/claude-plugin'); + $sdk->generate(__DIR__ . '/examples/claude-plugin'); } // Rust if (!$requestedSdk || $requestedSdk === 'rust') { $sdk = new SDK(new Rust(), new Swagger2($spec)); configureSDK($sdk); - generateExample($sdk, __DIR__ . '/examples/rust'); + $sdk->generate(__DIR__ . '/examples/rust'); } } catch (Exception $exception) { From 73f9020e8ed64fcae3feb738e1097be6dfc4009d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 4 May 2026 16:25:08 +0530 Subject: [PATCH 22/69] refactor(web): consolidate auth tiers into ClientAuth and ServerAuth - Rename ClientRuntime -> SDKPlatform and field runtime -> sdkPlatform - Remove ConsoleAuth type; merge cookie auth into ServerAuth (covers both console and SSR cookie-forwarding use cases) - Emit fromCookie on all platforms instead of console-only - Default mode: 'admin' in fromCookie on console builds so the wire request authenticates as admin without requiring callers to remember the X-Appwrite-Mode header - Add Prettify utility type and wrap factory params so IDE hover shows the full parameter shape instead of an opaque alias name - Simplify Web.php helpers (webServiceAuth, webMethodThisGate) by removing the platform argument now that ServerAuth covers all server-tier cases --- src/SDK/Language/Web.php | 58 +++++++---------- templates/web/src/client.ts.twig | 72 ++++++++++----------- templates/web/src/index.ts.twig | 2 +- templates/web/src/services/template.ts.twig | 12 ++-- 4 files changed, 65 insertions(+), 79 deletions(-) diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index a86494333a..6dbea995bf 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -476,14 +476,6 @@ public function getSubSchema(array $property, array $spec, string $methodName = return $this->getTypeName($property); } - /** - * Determine the TypeScript auth type name for a given platform. - */ - protected function webPlatformAuth(string $platform): string - { - return $platform === 'console' ? 'ConsoleAuth' : 'ServerAuth'; - } - /** * Determine whether a method supports client-side platforms. */ @@ -495,7 +487,7 @@ protected function methodSupportsClient(array $method): bool /** * Determine whether a method supports server/console platforms. */ - protected function methodSupportsPlatformAuth(array $method): bool + protected function methodSupportsServer(array $method): bool { $platforms = $method['platforms'] ?? []; return in_array('server', $platforms, true) || in_array('console', $platforms, true); @@ -506,7 +498,7 @@ protected function methodSupportsPlatformAuth(array $method): bool * * @return array */ - public function webServiceAuth(array $service, string $platform): array + public function webServiceAuth(array $service): array { $hasClientTier = false; $hasServerTier = false; @@ -515,20 +507,19 @@ public function webServiceAuth(array $service, string $platform): array $hasUpload = false; foreach ($service['methods'] ?? [] as $method) { - $platforms = $method['platforms'] ?? []; - $hasClient = in_array('client', $platforms, true); - $hasServerOrConsole = in_array('server', $platforms, true) || in_array('console', $platforms, true); + $hasClient = $this->methodSupportsClient($method); + $hasServer = $this->methodSupportsServer($method); if ($hasClient) { $hasClientTier = true; } - if ($hasServerOrConsole) { + if ($hasServer) { $hasServerTier = true; } - if ($hasServerOrConsole && !$hasClient) { + if ($hasServer && !$hasClient) { $hasServerOnly = true; } - if ($hasClient && !$hasServerOrConsole) { + if ($hasClient && !$hasServer) { $hasClientOnly = true; } if (in_array('multipart/form-data', $method['consumes'] ?? [], true)) { @@ -537,38 +528,33 @@ public function webServiceAuth(array $service, string $platform): array } $hasMixedTier = $hasClientTier && $hasServerTier; - $platformAuth = $this->webPlatformAuth($platform); return [ - 'hasClientTier' => $hasClientTier, - 'hasServerTier' => $hasServerTier, - 'hasMixedTier' => $hasMixedTier, - 'platformAuth' => $platformAuth, - 'needsPlatformAuth' => $hasServerTier && (!$hasMixedTier || $hasServerOnly), - 'needsClientAuth' => $hasClientTier && (!$hasMixedTier || $hasClientOnly), - 'hasUpload' => $hasUpload, + 'hasClientTier' => $hasClientTier, + 'hasServerTier' => $hasServerTier, + 'hasMixedTier' => $hasMixedTier, + 'needsServerAuth' => $hasServerTier && (!$hasMixedTier || $hasServerOnly), + 'needsClientAuth' => $hasClientTier && (!$hasMixedTier || $hasClientOnly), + 'hasUpload' => $hasUpload, ]; } /** * Build the TypeScript `this:` gate string for a method in a Web service. */ - public function webMethodThisGate(array $method, array $service, string $platform): string + public function webMethodThisGate(array $method, array $service): string { - $auth = $this->webServiceAuth($service, $platform); + $auth = $this->webServiceAuth($service); if (!$auth['hasMixedTier']) { return ''; } - $methodSupportsClient = $this->methodSupportsClient($method); - $methodSupportsPlatform = $this->methodSupportsPlatformAuth($method); - $serviceName = $this->toPascalCase($service['name'] ?? ''); - if (!$methodSupportsClient) { - return 'this: ' . $serviceName . '<' . $auth['platformAuth'] . '>, '; + if (!$this->methodSupportsClient($method)) { + return 'this: ' . $serviceName . ', '; } - if (!$methodSupportsPlatform) { + if (!$this->methodSupportsServer($method)) { return 'this: ' . $serviceName . ', '; } @@ -635,11 +621,11 @@ public function getFilters(): array return $condition; }, ['is_safe' => ['html']]), - new TwigFilter('webServiceAuth', function (array $service, string $platform) { - return $this->webServiceAuth($service, $platform); + new TwigFilter('webServiceAuth', function (array $service) { + return $this->webServiceAuth($service); }), - new TwigFilter('webMethodThisGate', function (array $method, array $service, string $platform) { - return $this->webMethodThisGate($method, $service, $platform); + new TwigFilter('webMethodThisGate', function (array $method, array $service) { + return $this->webMethodThisGate($method, $service); }, ['is_safe' => ['html']]), new TwigFilter('comment2', function ($value) { $value = explode("\n", $value); diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index dd3bdb2f4a..79d78f5c4c 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -344,11 +344,13 @@ class {{spec.title | caseUcfirst}}Exception extends Error { /** * Client that handles requests to {{spec.title | caseUcfirst}} */ -type ClientRuntime = 'client' | 'server'; +type SDKPlatform = 'client' | 'server'; type ClientAuth = 'browser' | 'session' | 'devKey' | 'impersonation'; -type ServerAuth = 'apiKey' | 'jwt'; -type ConsoleAuth = {% if sdk.platform == 'console' %}'apiKey' | 'cookie'{% else %}never{% endif %}; -type Auth = ClientAuth | ServerAuth | ConsoleAuth; +type ServerAuth = 'apiKey' | 'jwt' | 'cookie'; +type Auth = ClientAuth | ServerAuth; + +// Forces TypeScript to display the expanded shape on hover instead of an alias name. +type Prettify = { [K in keyof T]: T[K] } & {}; type BaseClientParams = { endpoint: string; @@ -369,12 +371,10 @@ type ApiKeyClientParams = BaseClientParams & { apiKey: string; }; -{%~ if sdk.platform == 'console' %} type CookieClientParams = BaseClientParams & { cookie: string; }; -{%~ endif %} type JWTClientParams = BaseClientParams & { jwt: string; }; @@ -401,8 +401,8 @@ class Client { endpointRealtime: string; project: string; key: string; -{%~ if sdk.platform == 'console' %} cookie: string; +{%~ if sdk.platform == 'console' %} mode: string; platform: string; {%~ endif %} @@ -420,8 +420,8 @@ class Client { endpointRealtime: '', project: '', key: '', -{%~ if sdk.platform == 'console' %} cookie: '', +{%~ if sdk.platform == 'console' %} mode: '', platform: '', {%~ endif %} @@ -436,7 +436,7 @@ class Client { selfSigned: false, }; - private runtime: ClientRuntime = '{{ sdk.platform == 'server' ? 'server' : 'client' }}'; + private sdkPlatform: SDKPlatform = '{{ sdk.platform == 'server' ? 'server' : 'client' }}'; /** * Custom headers for API requests. @@ -451,18 +451,18 @@ class Client { {%~ endfor %} }; - static fromBrowser(params: BaseClientParams): Client<'browser'> { + static fromBrowser(params: Prettify): Client<'browser'> { return new Client<'browser'>().applyBase<'browser'>(params, 'client'); } - static fromSession(params: SessionClientParams): Client<'session'> { + static fromSession(params: Prettify): Client<'session'> { return new Client<'session'>() .applyBase<'session'>(params, 'client') .setSession(params.session); } {%~ if sdk.platform == 'server' or sdk.platform == 'console' %} - static fromAPIKey(params: ApiKeyClientParams): Client<'apiKey'> { + static fromAPIKey(params: Prettify): Client<'apiKey'> { return new Client<'apiKey'>() .applyBase<'apiKey'>(params, 'server') .setKey(params.apiKey); @@ -470,27 +470,29 @@ class Client { {%~ endif %} -{%~ if sdk.platform == 'console' %} - static fromCookie(params: CookieClientParams): Client<'cookie'> { + static fromCookie(params: Prettify): Client<'cookie'> { return new Client<'cookie'>() +{%~ if sdk.platform == 'console' %} + .applyBase<'cookie'>({ mode: 'admin', ...params }, 'server') +{%~ else %} .applyBase<'cookie'>(params, 'server') +{%~ endif %} .setCookie(params.cookie); } -{%~ endif %} - static fromJWT(params: JWTClientParams): Client<'jwt'> { + static fromJWT(params: Prettify): Client<'jwt'> { return new Client<'jwt'>() .applyBase<'jwt'>(params, 'server') .setJWT(params.jwt); } - static fromDevKey(params: DevKeyClientParams): Client<'devKey'> { + static fromDevKey(params: Prettify): Client<'devKey'> { return new Client<'devKey'>() .applyBase<'devKey'>(params, 'client') .setDevKey(params.devKey); } - static fromImpersonation(params: ImpersonationClientParams): Client<'impersonation'> { + static fromImpersonation(params: Prettify): Client<'impersonation'> { const targets = [ params.userId !== undefined, params.email !== undefined, @@ -526,10 +528,10 @@ class Client { return this; } - private applyBase(params: BaseClientParams, runtime: ClientRuntime): Client { + private applyBase(params: BaseClientParams, sdkPlatform: SDKPlatform): Client { const client = this as unknown as Client; - client.runtime = runtime; - client.headers['x-sdk-platform'] = runtime === 'server' ? '{{ sdk.platform == 'console' ? 'console' : 'server' }}' : 'client'; + client.sdkPlatform = sdkPlatform; + client.headers['x-sdk-platform'] = sdkPlatform === 'server' ? '{{ sdk.platform == 'console' ? 'console' : 'server' }}' : 'client'; client.setEndpoint(params.endpoint); client.setProject(params.projectId); @@ -633,25 +635,23 @@ class Client { private setKey(value: string): Client<'apiKey'> { this.headers['X-{{ spec.title | caseUcfirst }}-Key'] = value; this.config.key = value; - this.runtime = 'server'; + this.sdkPlatform = 'server'; this.headers['x-sdk-platform'] = '{{ sdk.platform == 'console' ? 'console' : 'server' }}'; return this as unknown as Client<'apiKey'>; } -{%~ if sdk.platform == 'console' %} private setCookie(value: string): Client<'cookie'> { this.headers['Cookie'] = value; this.config.cookie = value; - this.runtime = 'server'; + this.sdkPlatform = 'server'; this.headers['x-sdk-platform'] = '{{ sdk.platform == 'console' ? 'console' : 'server' }}'; return this as unknown as Client<'cookie'>; } -{%~ endif %} private setJWT(value: string): Client<'jwt'> { this.headers['X-{{ spec.title | caseUcfirst }}-JWT'] = value; this.config.jwt = value; - this.runtime = 'server'; + this.sdkPlatform = 'server'; this.headers['x-sdk-platform'] = '{{ sdk.platform == 'console' ? 'console' : 'server' }}'; return this as unknown as Client<'jwt'>; } @@ -665,7 +665,7 @@ class Client { private setSession(value: string): Client<'session'> { this.headers['X-{{ spec.title | caseUcfirst }}-Session'] = value; this.config.session = value; - this.runtime = 'client'; + this.sdkPlatform = 'client'; this.headers['x-sdk-platform'] = 'client'; return this as unknown as Client<'session'>; } @@ -673,7 +673,7 @@ class Client { private setDevKey(value: string): Client<'devKey'> { this.headers['X-{{ spec.title | caseUcfirst }}-Dev-Key'] = value; this.config.devkey = value; - this.runtime = 'client'; + this.sdkPlatform = 'client'; this.headers['x-sdk-platform'] = 'client'; return this as unknown as Client<'devKey'>; } @@ -687,7 +687,7 @@ class Client { private setImpersonateUserId(value: string): Client<'impersonation'> { this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id'] = value; this.config.impersonateuserid = value; - this.runtime = 'client'; + this.sdkPlatform = 'client'; this.headers['x-sdk-platform'] = 'client'; return this as unknown as Client<'impersonation'>; } @@ -695,7 +695,7 @@ class Client { private setImpersonateUserEmail(value: string): Client<'impersonation'> { this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email'] = value; this.config.impersonateuseremail = value; - this.runtime = 'client'; + this.sdkPlatform = 'client'; this.headers['x-sdk-platform'] = 'client'; return this as unknown as Client<'impersonation'>; } @@ -703,7 +703,7 @@ class Client { private setImpersonateUserPhone(value: string): Client<'impersonation'> { this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone'] = value; this.config.impersonateuserphone = value; - this.runtime = 'client'; + this.sdkPlatform = 'client'; this.headers['x-sdk-platform'] = 'client'; return this as unknown as Client<'impersonation'>; } @@ -1014,7 +1014,7 @@ class Client { headers = Object.assign({}, this.headers, headers); - if (this.runtime === 'client' && typeof window !== 'undefined' && window.localStorage) { + if (this.sdkPlatform === 'client' && typeof window !== 'undefined' && window.localStorage) { const cookieFallback = window.localStorage.getItem('cookieFallback'); if (cookieFallback) { headers['X-Fallback-Cookies'] = cookieFallback; @@ -1026,7 +1026,7 @@ class Client { headers, }; - if (this.runtime === 'client' && headers['X-Appwrite-Dev-Key'] === undefined) { + if (this.sdkPlatform === 'client' && headers['X-Appwrite-Dev-Key'] === undefined) { options.credentials = 'include'; } @@ -1133,7 +1133,7 @@ class Client { const response = await fetch(uri, options); // type opaque: No-CORS, different-origin response (CORS-issue) - if (this.runtime === 'client' && typeof window !== 'undefined' && response.type === 'opaque') { + if (this.sdkPlatform === 'client' && typeof window !== 'undefined' && response.type === 'opaque') { throw new {{spec.title | caseUcfirst}}Exception( `Invalid Origin. Register your new client (${typeof window !== 'undefined' ? window.location.host : 'unknown'}) as a new Web platform on your project console dashboard`, 403, @@ -1169,7 +1169,7 @@ class Client { const cookieFallback = response.headers.get('X-Fallback-Cookies'); - if (this.runtime === 'client' && typeof window !== 'undefined' && window.localStorage && cookieFallback) { + if (this.sdkPlatform === 'client' && typeof window !== 'undefined' && window.localStorage && cookieFallback) { window.console.warn('{{spec.title | caseUcfirst}} is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); window.localStorage.setItem('cookieFallback', cookieFallback); } @@ -1198,6 +1198,6 @@ class Client { } export { Client, {{spec.title | caseUcfirst}}Exception }; -export type { Models, ClientAuth, ServerAuth, ConsoleAuth, Payload, RealtimeResponseEvent, UploadProgress }; +export type { Models, SDKPlatform, ClientAuth, ServerAuth, Payload, RealtimeResponseEvent, UploadProgress }; export { Query } from './query'; export type { QueryTypes, QueryTypesList } from './query'; diff --git a/templates/web/src/index.ts.twig b/templates/web/src/index.ts.twig index 8a8a978584..ce5c36a6d3 100644 --- a/templates/web/src/index.ts.twig +++ b/templates/web/src/index.ts.twig @@ -10,7 +10,7 @@ export { Client, Query, {{spec.title | caseUcfirst}}Exception } from './client'; export { {{service.name | caseUcfirst}} } from './services/{{service.name | caseKebab}}'; {% endfor %} export { Realtime } from './services/realtime'; -export type { Models, Payload, RealtimeResponseEvent, UploadProgress, ClientAuth, ServerAuth, ConsoleAuth } from './client'; +export type { Models, Payload, RealtimeResponseEvent, UploadProgress, SDKPlatform, ClientAuth, ServerAuth } from './client'; export type { RealtimeSubscription } from './services/realtime'; export type { QueryTypes, QueryTypesList } from './query'; export { Permission } from './permission'; diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index da884f78be..d1b8d8dcfd 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -1,7 +1,7 @@ {# Detect service shape and imports before emitting TypeScript. #} -{% set auth = service | webServiceAuth(sdk.platform) %} +{% set auth = service | webServiceAuth %} import { Service } from '../service'; -import { {{ spec.title | caseUcfirst}}Exception, Client, {% if auth.needsClientAuth or auth.hasMixedTier %}type ClientAuth, {% endif %}{% if auth.needsPlatformAuth or auth.hasMixedTier %}type {{ auth.platformAuth }}, {% endif %}type Payload{% if auth.hasUpload %}, UploadProgress{% endif %} } from '../client'; +import { {{ spec.title | caseUcfirst}}Exception, Client, {% if auth.needsClientAuth or auth.hasMixedTier %}type ClientAuth, {% endif %}{% if auth.needsServerAuth or auth.hasMixedTier %}type ServerAuth, {% endif %}type Payload{% if auth.hasUpload %}, UploadProgress{% endif %} } from '../client'; import type { Models } from '../models'; {% set added = [] %} @@ -17,7 +17,7 @@ import { {{ parameter.enumName | caseUcfirst }} } from '../enums/{{ parameter.en {% endfor %} {%~ if auth.hasMixedTier %} -export class {{ service.name | caseUcfirst }} { +export class {{ service.name | caseUcfirst }} { client: Client; constructor(client: Client) { @@ -25,9 +25,9 @@ export class {{ service.name | caseUcfirst }}; + client: Client; - constructor(client: Client<{{ auth.platformAuth }}>) { + constructor(client: Client) { this.client = client; } {%~ else %} @@ -40,7 +40,7 @@ export class {{ service.name | caseUcfirst }} { {%~ endif %} {%~ for method in service.methods %} - {%~ set thisGate = method | webMethodThisGate(service, sdk.platform) %} + {%~ set thisGate = method | webMethodThisGate(service) %} /** {%~ if method.description %} * {{ method.description | replace({'\n': '\n * '}) | raw }} From 7d2e645ac5b613861aa10cc3c3dc0744a856293a Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 4 May 2026 16:38:10 +0530 Subject: [PATCH 23/69] refactor(web): guard fromJWT to server/console + expose selfSigned param - Wrap fromJWT in a platform guard mirroring fromAPIKey. JWT auth lives in ServerAuth, so emitting fromJWT on client builds produced a dead factory: the returned Client<'jwt'> could not satisfy any service generated from the client spec (which only carry ClientAuth). - Add selfSigned?: boolean to BaseClientParams and apply it inside applyBase so every factory gets a public migration path. Previously setSelfSigned was the only way to set the flag and was made private by the factory refactor, leaving callers without a public hook. --- templates/web/src/client.ts.twig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 79d78f5c4c..4ec3f5a508 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -357,6 +357,7 @@ type BaseClientParams = { projectId: string; endpointRealtime?: string; locale?: string; + selfSigned?: boolean; {%~ if sdk.platform == 'console' %} mode?: string; platform?: string; @@ -480,12 +481,15 @@ class Client { .setCookie(params.cookie); } +{%~ if sdk.platform == 'server' or sdk.platform == 'console' %} static fromJWT(params: Prettify): Client<'jwt'> { return new Client<'jwt'>() .applyBase<'jwt'>(params, 'server') .setJWT(params.jwt); } +{%~ endif %} + static fromDevKey(params: Prettify): Client<'devKey'> { return new Client<'devKey'>() .applyBase<'devKey'>(params, 'client') @@ -543,6 +547,10 @@ class Client { client.setLocale(params.locale); } + if (params.selfSigned !== undefined) { + client.setSelfSigned(params.selfSigned); + } + {%~ if sdk.platform == 'console' %} if (params.mode !== undefined) { client.headers['X-{{ spec.title | caseUcfirst }}-Mode'] = params.mode; From a47f9d7cc0186c85f93730769ec5bfbdf97b42ff Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 4 May 2026 17:01:10 +0530 Subject: [PATCH 24/69] refactor(web): spec-drive client config and auth setters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the hardcoded config block and ten manually-written auth setters with a single spec.global.headers loop, matching the pattern every other SDK template uses (Python, Dart, Kotlin, etc.). Setters become primitive: header write + config write + return this. The redundant sdkPlatform / x-sdk-platform writes are removed because applyBase already sets both before the setter runs, and the setters are private — only callable from inside factories. Dropping the duplication also lets the typed Client<'apiKey'> etc. flow through chained calls without `as unknown as Client<...>` casts. Each platform spec carries a different subset of securityDefinitions, so a Web.php Twig filter (webClientHeaders) augments the parsed list with auth headers the unified client needs but the loaded spec omits (e.g. Session/DevKey on console, Cookie on client). The filter has a TODO pointing at appwrite/appwrite#12211, which moves the union into each platform spec's securityDefinitions directly. Once that ships and specs regenerate, the filter and its registration can be deleted in a follow-up. Verified against console, client, and server builds plus an end-to-end smoke test calling Account.get() through fromCookie on Appwrite Cloud. --- src/SDK/Language/Web.php | 44 ++++++++++++ templates/web/src/client.ts.twig | 119 ++++--------------------------- 2 files changed, 58 insertions(+), 105 deletions(-) diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index 6dbea995bf..e89e621b6d 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -476,6 +476,47 @@ public function getSubSchema(array $property, array $spec, string $methodName = return $this->getTypeName($property); } + /** + * Augment spec.global.headers with any auth headers required by the unified + * web client but missing from the loaded spec. Each platform's spec exposes + * a different subset of securityDefinitions (e.g. console omits Session and + * DevKey; client omits Cookie and Mode), but the unified web Client emits + * setters for the union so factories work regardless of build target. + * + * TODO: Remove this augmentation once appwrite/appwrite#12211 ships and the + * regenerated specs declare the union of auth headers in every platform's + * securityDefinitions. After that, spec.global.headers will already carry + * everything the unified client needs and this filter (plus its Twig + * registration) can be deleted. + * + * @param array $globalHeaders headers parsed from securityDefinitions + * @return array merged list, preserving spec entries (with descriptions) + */ + public function webClientHeaders(array $globalHeaders): array + { + $required = [ + 'Project' => 'X-Appwrite-Project', + 'Key' => 'X-Appwrite-Key', + 'Cookie' => 'Cookie', + 'JWT' => 'X-Appwrite-JWT', + 'Locale' => 'X-Appwrite-Locale', + 'Session' => 'X-Appwrite-Session', + 'DevKey' => 'X-Appwrite-Dev-Key', + 'Mode' => 'X-Appwrite-Mode', + 'Platform' => 'X-Appwrite-Platform', + 'ImpersonateUserId' => 'X-Appwrite-Impersonate-User-Id', + 'ImpersonateUserEmail' => 'X-Appwrite-Impersonate-User-Email', + 'ImpersonateUserPhone' => 'X-Appwrite-Impersonate-User-Phone', + ]; + $existing = array_column($globalHeaders, 'key'); + foreach ($required as $key => $name) { + if (!in_array($key, $existing, true)) { + $globalHeaders[] = ['key' => $key, 'name' => $name, 'description' => '']; + } + } + return $globalHeaders; + } + /** * Determine whether a method supports client-side platforms. */ @@ -627,6 +668,9 @@ public function getFilters(): array new TwigFilter('webMethodThisGate', function (array $method, array $service) { return $this->webMethodThisGate($method, $service); }, ['is_safe' => ['html']]), + new TwigFilter('webClientHeaders', function (array $globalHeaders) { + return $this->webClientHeaders($globalHeaders); + }), new TwigFilter('comment2', function ($value) { $value = explode("\n", $value); foreach ($value as $key => $line) { diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 4ec3f5a508..a1f63ac54f 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -391,6 +391,7 @@ type ImpersonationTarget = type ImpersonationClientParams = SessionClientParams & ImpersonationTarget; +{%~ set webHeaders = spec.global.headers | webClientHeaders %} class Client { static CHUNK_SIZE = 1024 * 1024 * 5; @@ -400,40 +401,18 @@ class Client { config: { endpoint: string; endpointRealtime: string; - project: string; - key: string; - cookie: string; -{%~ if sdk.platform == 'console' %} - mode: string; - platform: string; -{%~ endif %} - jwt: string; - locale: string; - session: string; - devkey: string; +{%~ for header in webHeaders %} + {{ header.key | caseLower }}: string; +{%~ endfor %} forwardeduseragent: string; - impersonateuserid: string; - impersonateuseremail: string; - impersonateuserphone: string; selfSigned: boolean; } = { endpoint: '{{ spec.endpoint }}', endpointRealtime: '', - project: '', - key: '', - cookie: '', -{%~ if sdk.platform == 'console' %} - mode: '', - platform: '', -{%~ endif %} - jwt: '', - locale: '', - session: '', - devkey: '', +{%~ for header in webHeaders %} + {{ header.key | caseLower }}: '', +{%~ endfor %} forwardeduseragent: '', - impersonateuserid: '', - impersonateuseremail: '', - impersonateuserphone: '', selfSigned: false, }; @@ -553,13 +532,11 @@ class Client { {%~ if sdk.platform == 'console' %} if (params.mode !== undefined) { - client.headers['X-{{ spec.title | caseUcfirst }}-Mode'] = params.mode; - client.config.mode = params.mode; + client.setMode(params.mode); } if (params.platform !== undefined) { - client.headers['X-{{ spec.title | caseUcfirst }}-Platform'] = params.platform; - client.config.platform = params.platform; + client.setPlatform(params.platform); } {%~ endif %} @@ -634,88 +611,20 @@ class Client { return this; } - private setProject(value: string): this { - this.headers['X-{{ spec.title | caseUcfirst }}-Project'] = value; - this.config.project = value; +{%~ for header in webHeaders %} + private set{{ header.key | caseUcfirst }}(value: string): this { + this.headers['{{ header.name }}'] = value; + this.config.{{ header.key | caseLower }} = value; return this; } - private setKey(value: string): Client<'apiKey'> { - this.headers['X-{{ spec.title | caseUcfirst }}-Key'] = value; - this.config.key = value; - this.sdkPlatform = 'server'; - this.headers['x-sdk-platform'] = '{{ sdk.platform == 'console' ? 'console' : 'server' }}'; - return this as unknown as Client<'apiKey'>; - } - - private setCookie(value: string): Client<'cookie'> { - this.headers['Cookie'] = value; - this.config.cookie = value; - this.sdkPlatform = 'server'; - this.headers['x-sdk-platform'] = '{{ sdk.platform == 'console' ? 'console' : 'server' }}'; - return this as unknown as Client<'cookie'>; - } - - private setJWT(value: string): Client<'jwt'> { - this.headers['X-{{ spec.title | caseUcfirst }}-JWT'] = value; - this.config.jwt = value; - this.sdkPlatform = 'server'; - this.headers['x-sdk-platform'] = '{{ sdk.platform == 'console' ? 'console' : 'server' }}'; - return this as unknown as Client<'jwt'>; - } - - private setLocale(value: string): this { - this.headers['X-{{ spec.title | caseUcfirst }}-Locale'] = value; - this.config.locale = value; - return this; - } - - private setSession(value: string): Client<'session'> { - this.headers['X-{{ spec.title | caseUcfirst }}-Session'] = value; - this.config.session = value; - this.sdkPlatform = 'client'; - this.headers['x-sdk-platform'] = 'client'; - return this as unknown as Client<'session'>; - } - - private setDevKey(value: string): Client<'devKey'> { - this.headers['X-{{ spec.title | caseUcfirst }}-Dev-Key'] = value; - this.config.devkey = value; - this.sdkPlatform = 'client'; - this.headers['x-sdk-platform'] = 'client'; - return this as unknown as Client<'devKey'>; - } - +{%~ endfor %} private setForwardedUserAgent(value: string): this { this.headers['X-Forwarded-User-Agent'] = value; this.config.forwardeduseragent = value; return this; } - private setImpersonateUserId(value: string): Client<'impersonation'> { - this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id'] = value; - this.config.impersonateuserid = value; - this.sdkPlatform = 'client'; - this.headers['x-sdk-platform'] = 'client'; - return this as unknown as Client<'impersonation'>; - } - - private setImpersonateUserEmail(value: string): Client<'impersonation'> { - this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email'] = value; - this.config.impersonateuseremail = value; - this.sdkPlatform = 'client'; - this.headers['x-sdk-platform'] = 'client'; - return this as unknown as Client<'impersonation'>; - } - - private setImpersonateUserPhone(value: string): Client<'impersonation'> { - this.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone'] = value; - this.config.impersonateuserphone = value; - this.sdkPlatform = 'client'; - this.headers['x-sdk-platform'] = 'client'; - return this as unknown as Client<'impersonation'>; - } - private realtime: Realtime = { socket: undefined, timeout: undefined, From 8907d83f98b06bd803357a27cad00a98e8759a50 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 4 May 2026 17:05:40 +0530 Subject: [PATCH 25/69] fix(web): drop duplicate forwardeduseragent setter on server build The server platform spec includes ForwardedUserAgent in securityDefinitions, so the new spec.global.headers loop generates a config field and setForwardedUserAgent setter for it on server builds. That collided with the manual versions left over in the template, producing TS2300/TS1117/TS2393 duplicate-identifier errors when running tsc --declaration in the web (server) CI job. Add ForwardedUserAgent to webClientHeaders so it is universally present across all platform builds (the unified web client always exposes withForwardedUserAgent for chained user-agent forwarding) and remove the manual config field and setter from the template. The loop now owns it on every build target. Verified npm run build:types passes for web (server), web (console), and web (client) on a clean examples/web tree. --- src/SDK/Language/Web.php | 1 + templates/web/src/client.ts.twig | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index e89e621b6d..90d3f157c1 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -504,6 +504,7 @@ public function webClientHeaders(array $globalHeaders): array 'DevKey' => 'X-Appwrite-Dev-Key', 'Mode' => 'X-Appwrite-Mode', 'Platform' => 'X-Appwrite-Platform', + 'ForwardedUserAgent' => 'X-Forwarded-User-Agent', 'ImpersonateUserId' => 'X-Appwrite-Impersonate-User-Id', 'ImpersonateUserEmail' => 'X-Appwrite-Impersonate-User-Email', 'ImpersonateUserPhone' => 'X-Appwrite-Impersonate-User-Phone', diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index a1f63ac54f..13faa3b638 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -404,7 +404,6 @@ class Client { {%~ for header in webHeaders %} {{ header.key | caseLower }}: string; {%~ endfor %} - forwardeduseragent: string; selfSigned: boolean; } = { endpoint: '{{ spec.endpoint }}', @@ -412,7 +411,6 @@ class Client { {%~ for header in webHeaders %} {{ header.key | caseLower }}: '', {%~ endfor %} - forwardeduseragent: '', selfSigned: false, }; @@ -619,11 +617,6 @@ class Client { } {%~ endfor %} - private setForwardedUserAgent(value: string): this { - this.headers['X-Forwarded-User-Agent'] = value; - this.config.forwardeduseragent = value; - return this; - } private realtime: Realtime = { socket: undefined, From 73f0a60503ba2fe78783426a3db9cc5808d34a49 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 6 May 2026 15:10:44 +0530 Subject: [PATCH 26/69] Preserve legacy web client setters --- templates/web/src/client.ts.twig | 75 +++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 13faa3b638..cb5ec6a0f5 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -392,7 +392,28 @@ type ImpersonationTarget = type ImpersonationClientParams = SessionClientParams & ImpersonationTarget; {%~ set webHeaders = spec.global.headers | webClientHeaders %} -class Client { +type LegacyClientSetter = Extract, `set${string}`>; +export type Client = Omit, LegacyClientSetter>; +type LegacyClient = ClientRuntime; + +type ClientConstructor = { + new (): LegacyClient; + readonly CHUNK_SIZE: number; + fromBrowser(params: Prettify): Client<'browser'>; + fromSession(params: Prettify): Client<'session'>; +{%~ if sdk.platform == 'server' or sdk.platform == 'console' %} + fromAPIKey(params: Prettify): Client<'apiKey'>; +{%~ endif %} + fromCookie(params: Prettify): Client<'cookie'>; +{%~ if sdk.platform == 'server' or sdk.platform == 'console' %} + fromJWT(params: Prettify): Client<'jwt'>; +{%~ endif %} + fromDevKey(params: Prettify): Client<'devKey'>; + fromImpersonation(params: Prettify): Client<'impersonation'>; + flatten(data: Payload, prefix?: string): Payload; +}; + +class ClientRuntime { static CHUNK_SIZE = 1024 * 1024 * 5; /** @@ -430,18 +451,18 @@ class Client { }; static fromBrowser(params: Prettify): Client<'browser'> { - return new Client<'browser'>().applyBase<'browser'>(params, 'client'); + return new ClientRuntime<'browser'>().applyBase<'browser'>(params, 'client'); } static fromSession(params: Prettify): Client<'session'> { - return new Client<'session'>() + return new ClientRuntime<'session'>() .applyBase<'session'>(params, 'client') .setSession(params.session); } {%~ if sdk.platform == 'server' or sdk.platform == 'console' %} static fromAPIKey(params: Prettify): Client<'apiKey'> { - return new Client<'apiKey'>() + return new ClientRuntime<'apiKey'>() .applyBase<'apiKey'>(params, 'server') .setKey(params.apiKey); } @@ -449,7 +470,7 @@ class Client { {%~ endif %} static fromCookie(params: Prettify): Client<'cookie'> { - return new Client<'cookie'>() + return new ClientRuntime<'cookie'>() {%~ if sdk.platform == 'console' %} .applyBase<'cookie'>({ mode: 'admin', ...params }, 'server') {%~ else %} @@ -460,7 +481,7 @@ class Client { {%~ if sdk.platform == 'server' or sdk.platform == 'console' %} static fromJWT(params: Prettify): Client<'jwt'> { - return new Client<'jwt'>() + return new ClientRuntime<'jwt'>() .applyBase<'jwt'>(params, 'server') .setJWT(params.jwt); } @@ -468,7 +489,7 @@ class Client { {%~ endif %} static fromDevKey(params: Prettify): Client<'devKey'> { - return new Client<'devKey'>() + return new ClientRuntime<'devKey'>() .applyBase<'devKey'>(params, 'client') .setDevKey(params.devKey); } @@ -484,7 +505,7 @@ class Client { throw new {{spec.title | caseUcfirst}}Exception('Exactly one impersonation target must be provided'); } - const client = new Client<'impersonation'>() + const client = new ClientRuntime<'impersonation'>() .applyBase<'impersonation'>(params, 'client') .setSession(params.session); @@ -509,8 +530,8 @@ class Client { return this; } - private applyBase(params: BaseClientParams, sdkPlatform: SDKPlatform): Client { - const client = this as unknown as Client; + private applyBase(params: BaseClientParams, sdkPlatform: SDKPlatform): ClientRuntime { + const client = this as unknown as ClientRuntime; client.sdkPlatform = sdkPlatform; client.headers['x-sdk-platform'] = sdkPlatform === 'server' ? '{{ sdk.platform == 'console' ? 'console' : 'server' }}' : 'client'; client.setEndpoint(params.endpoint); @@ -562,7 +583,10 @@ class Client { * * @returns {this} */ - private setEndpoint(endpoint: string): this { + /** + * @deprecated Use `Client.fromBrowser`, `Client.fromSession`, `Client.fromAPIKey`, or another static factory instead. + */ + setEndpoint(endpoint: string): this { if (!endpoint || typeof endpoint !== 'string') { throw new {{spec.title | caseUcfirst}}Exception('Endpoint must be a valid string'); } @@ -584,7 +608,10 @@ class Client { * * @returns {this} */ - private setEndpointRealtime(endpointRealtime: string): this { + /** + * @deprecated Use the `endpointRealtime` field on a static factory params object instead. + */ + setEndpointRealtime(endpointRealtime: string): this { if (!endpointRealtime || typeof endpointRealtime !== 'string') { throw new {{spec.title | caseUcfirst}}Exception('Endpoint must be a valid string'); } @@ -604,16 +631,22 @@ class Client { * * @returns {this} */ - private setSelfSigned(selfSigned: boolean): this { + /** + * @deprecated Use the `selfSigned` field on a static factory params object instead. + */ + setSelfSigned(selfSigned: boolean): this { this.config.selfSigned = selfSigned; return this; } {%~ for header in webHeaders %} - private set{{ header.key | caseUcfirst }}(value: string): this { + /** + * @deprecated Use a static client factory or factory params object instead. + */ + set{{ header.key | caseUcfirst }}(value: string): {% if header.key == 'Key' %}ClientRuntime<'apiKey'>{% elseif header.key == 'Cookie' %}ClientRuntime<'cookie'>{% elseif header.key == 'JWT' %}ClientRuntime<'jwt'>{% elseif header.key == 'Session' %}ClientRuntime<'session'>{% elseif header.key == 'DevKey' %}ClientRuntime<'devKey'>{% elseif header.key in ['ImpersonateUserId', 'ImpersonateUserEmail', 'ImpersonateUserPhone'] %}ClientRuntime<'impersonation'>{% else %}this{% endif %} { this.headers['{{ header.name }}'] = value; this.config.{{ header.key | caseLower }} = value; - return this; + return this as unknown as {% if header.key == 'Key' %}ClientRuntime<'apiKey'>{% elseif header.key == 'Cookie' %}ClientRuntime<'cookie'>{% elseif header.key == 'JWT' %}ClientRuntime<'jwt'>{% elseif header.key == 'Session' %}ClientRuntime<'session'>{% elseif header.key == 'DevKey' %}ClientRuntime<'devKey'>{% elseif header.key in ['ImpersonateUserId', 'ImpersonateUserEmail', 'ImpersonateUserPhone'] %}ClientRuntime<'impersonation'>{% else %}this{% endif %}; } {%~ endfor %} @@ -941,7 +974,7 @@ class Client { } if (method === 'GET') { - for (const [key, value] of Object.entries(Client.flatten(params))) { + for (const [key, value] of Object.entries(ClientRuntime.flatten(params))) { url.searchParams.append(key, value); } } else { @@ -989,7 +1022,7 @@ class Client { throw new Error('File not found in payload'); } - if (file.size <= Client.CHUNK_SIZE) { + if (file.size <= ClientRuntime.CHUNK_SIZE) { return await this.call(method, url, headers, originalPayload); } @@ -1016,8 +1049,8 @@ class Client { $id: response.$id, progress: Math.round((end / file.size) * 100), sizeUploaded: end, - chunksTotal: Math.ceil(file.size / Client.CHUNK_SIZE), - chunksUploaded: Math.ceil(end / Client.CHUNK_SIZE) + chunksTotal: Math.ceil(file.size / ClientRuntime.CHUNK_SIZE), + chunksUploaded: Math.ceil(end / ClientRuntime.CHUNK_SIZE) }); } @@ -1097,7 +1130,7 @@ class Client { for (const [key, value] of Object.entries(data)) { let finalKey = prefix ? prefix + '[' + key +']' : key; if (Array.isArray(value)) { - output = { ...output, ...Client.flatten(value, finalKey) }; + output = { ...output, ...ClientRuntime.flatten(value, finalKey) }; } else { output[finalKey] = value; } @@ -1107,6 +1140,8 @@ class Client { } } +const Client = ClientRuntime as unknown as ClientConstructor; + export { Client, {{spec.title | caseUcfirst}}Exception }; export type { Models, SDKPlatform, ClientAuth, ServerAuth, Payload, RealtimeResponseEvent, UploadProgress }; export { Query } from './query'; From fd3a3a6e6fab35b4f1c28fb033cc80b505d0b8db Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 6 May 2026 15:32:17 +0530 Subject: [PATCH 27/69] Hide unauthorized web service methods --- src/SDK/Language/Web.php | 7 +++++ templates/web/src/client.ts.twig | 2 ++ templates/web/src/services/template.ts.twig | 34 ++++++++++++++++++--- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index 90d3f157c1..9a019cb8ea 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -547,10 +547,13 @@ public function webServiceAuth(array $service): array $hasServerOnly = false; $hasClientOnly = false; $hasUpload = 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; @@ -560,9 +563,11 @@ public function webServiceAuth(array $service): array } 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; @@ -578,6 +583,8 @@ public function webServiceAuth(array $service): array 'needsServerAuth' => $hasServerTier && (!$hasMixedTier || $hasServerOnly), 'needsClientAuth' => $hasClientTier && (!$hasMixedTier || $hasClientOnly), 'hasUpload' => $hasUpload, + 'serverOnlyMethods' => array_values(array_unique($serverOnlyMethods)), + 'clientOnlyMethods' => array_values(array_unique($clientOnlyMethods)), ]; } diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index cb5ec6a0f5..e294069c70 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -348,6 +348,7 @@ type SDKPlatform = 'client' | 'server'; type ClientAuth = 'browser' | 'session' | 'devKey' | 'impersonation'; type ServerAuth = 'apiKey' | 'jwt' | 'cookie'; type Auth = ClientAuth | ServerAuth; +declare const clientAuthBrand: unique symbol; // Forces TypeScript to display the expanded shape on hover instead of an alias name. type Prettify = { [K in keyof T]: T[K] } & {}; @@ -415,6 +416,7 @@ type ClientConstructor = { class ClientRuntime { static CHUNK_SIZE = 1024 * 1024 * 5; + declare readonly [clientAuthBrand]?: TAuth; /** * Holds configuration such as project. diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index d1b8d8dcfd..51b8f8c80c 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -17,21 +17,35 @@ import { {{ parameter.enumName | caseUcfirst }} } from '../enums/{{ parameter.en {% endfor %} {%~ if auth.hasMixedTier %} -export class {{ service.name | caseUcfirst }} { +type {{ service.name | caseUcfirst }}ServerOnlyMethod = {% if auth.serverOnlyMethods is empty %}never{% else %}{% for methodName in auth.serverOnlyMethods %}'{{ methodName }}'{% if not loop.last %} | {% endif %}{% endfor %}{% endif %}; +type {{ service.name | caseUcfirst }}ClientOnlyMethod = {% if auth.clientOnlyMethods is empty %}never{% else %}{% for methodName in auth.clientOnlyMethods %}'{{ methodName }}'{% if not loop.last %} | {% endif %}{% endfor %}{% endif %}; + +export type {{ service.name | caseUcfirst }} = + TAuth extends ClientAuth + ? Omit<{{ service.name | caseUcfirst }}Runtime, 'client' | {{ service.name | caseUcfirst }}ServerOnlyMethod> + : TAuth extends ServerAuth + ? Omit<{{ service.name | caseUcfirst }}Runtime, 'client' | {{ service.name | caseUcfirst }}ClientOnlyMethod> + : Omit<{{ service.name | caseUcfirst }}Runtime, 'client'>; + +class {{ service.name | caseUcfirst }}Runtime { client: Client; constructor(client: Client) { this.client = client; } {%~ elseif auth.hasServerTier %} -export class {{ service.name | caseUcfirst }} { +export type {{ service.name | caseUcfirst }} = Omit<{{ service.name | caseUcfirst }}Runtime, 'client'>; + +class {{ service.name | caseUcfirst }}Runtime { client: Client; constructor(client: Client) { this.client = client; } {%~ else %} -export class {{ service.name | caseUcfirst }} { +export type {{ service.name | caseUcfirst }} = Omit<{{ service.name | caseUcfirst }}Runtime, 'client'>; + +class {{ service.name | caseUcfirst }}Runtime { client: Client; constructor(client: Client) { @@ -40,7 +54,7 @@ export class {{ service.name | caseUcfirst }} { {%~ endif %} {%~ for method in service.methods %} - {%~ set thisGate = method | webMethodThisGate(service) %} + {%~ set thisGate = '' %} /** {%~ if method.description %} * {{ method.description | replace({'\n': '\n * '}) | raw }} @@ -193,3 +207,15 @@ export class {{ service.name | caseUcfirst }} { {%~ endif %} {%~ endfor %} } + +const {{ service.name | caseUcfirst }} = {{ service.name | caseUcfirst }}Runtime as unknown as { +{%~ if auth.hasMixedTier %} + new (client: Client): {{ service.name | caseUcfirst }}; +{%~ elseif auth.hasServerTier %} + new (client: Client): {{ service.name | caseUcfirst }}; +{%~ else %} + new (client: Client): {{ service.name | caseUcfirst }}; +{%~ endif %} +}; + +export { {{ service.name | caseUcfirst }} }; From 23661a655d7ff1f99de46a758b4dcb83fc446d99 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 6 May 2026 15:42:41 +0530 Subject: [PATCH 28/69] Hide internal web client helpers --- templates/web/src/client.ts.twig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index e294069c70..952732fd96 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -394,12 +394,13 @@ type ImpersonationClientParams = SessionClientParams & ImpersonationTarget; {%~ set webHeaders = spec.global.headers | webClientHeaders %} type LegacyClientSetter = Extract, `set${string}`>; -export type Client = Omit, LegacyClientSetter>; -type LegacyClient = ClientRuntime; +type ClientAuthBuilder = Extract, `with${string}`>; +type ClientInternalMethod = LegacyClientSetter | ClientAuthBuilder; +export type Client = Omit, ClientInternalMethod>; +type LegacyClient = Omit, ClientAuthBuilder>; type ClientConstructor = { new (): LegacyClient; - readonly CHUNK_SIZE: number; fromBrowser(params: Prettify): Client<'browser'>; fromSession(params: Prettify): Client<'session'>; {%~ if sdk.platform == 'server' or sdk.platform == 'console' %} @@ -411,7 +412,6 @@ type ClientConstructor = { {%~ endif %} fromDevKey(params: Prettify): Client<'devKey'>; fromImpersonation(params: Prettify): Client<'impersonation'>; - flatten(data: Payload, prefix?: string): Payload; }; class ClientRuntime { @@ -1032,7 +1032,7 @@ class ClientRuntime { let response = null; while (start < file.size) { - let end = start + Client.CHUNK_SIZE; // Prepare end for the next chunk + let end = start + ClientRuntime.CHUNK_SIZE; // Prepare end for the next chunk if (end >= file.size) { end = file.size; // Adjust for the last chunk to include the last byte } From 294cc8b0851b68e2d2d60a644b95a53bcce2d8da Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 6 May 2026 15:48:40 +0530 Subject: [PATCH 29/69] Remove web client header workaround --- src/SDK/Language/Web.php | 45 -------------------------------- templates/web/src/client.ts.twig | 3 +-- 2 files changed, 1 insertion(+), 47 deletions(-) diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index 9a019cb8ea..ff53ddb647 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -476,48 +476,6 @@ public function getSubSchema(array $property, array $spec, string $methodName = return $this->getTypeName($property); } - /** - * Augment spec.global.headers with any auth headers required by the unified - * web client but missing from the loaded spec. Each platform's spec exposes - * a different subset of securityDefinitions (e.g. console omits Session and - * DevKey; client omits Cookie and Mode), but the unified web Client emits - * setters for the union so factories work regardless of build target. - * - * TODO: Remove this augmentation once appwrite/appwrite#12211 ships and the - * regenerated specs declare the union of auth headers in every platform's - * securityDefinitions. After that, spec.global.headers will already carry - * everything the unified client needs and this filter (plus its Twig - * registration) can be deleted. - * - * @param array $globalHeaders headers parsed from securityDefinitions - * @return array merged list, preserving spec entries (with descriptions) - */ - public function webClientHeaders(array $globalHeaders): array - { - $required = [ - 'Project' => 'X-Appwrite-Project', - 'Key' => 'X-Appwrite-Key', - 'Cookie' => 'Cookie', - 'JWT' => 'X-Appwrite-JWT', - 'Locale' => 'X-Appwrite-Locale', - 'Session' => 'X-Appwrite-Session', - 'DevKey' => 'X-Appwrite-Dev-Key', - 'Mode' => 'X-Appwrite-Mode', - 'Platform' => 'X-Appwrite-Platform', - 'ForwardedUserAgent' => 'X-Forwarded-User-Agent', - 'ImpersonateUserId' => 'X-Appwrite-Impersonate-User-Id', - 'ImpersonateUserEmail' => 'X-Appwrite-Impersonate-User-Email', - 'ImpersonateUserPhone' => 'X-Appwrite-Impersonate-User-Phone', - ]; - $existing = array_column($globalHeaders, 'key'); - foreach ($required as $key => $name) { - if (!in_array($key, $existing, true)) { - $globalHeaders[] = ['key' => $key, 'name' => $name, 'description' => '']; - } - } - return $globalHeaders; - } - /** * Determine whether a method supports client-side platforms. */ @@ -676,9 +634,6 @@ public function getFilters(): array new TwigFilter('webMethodThisGate', function (array $method, array $service) { return $this->webMethodThisGate($method, $service); }, ['is_safe' => ['html']]), - new TwigFilter('webClientHeaders', function (array $globalHeaders) { - return $this->webClientHeaders($globalHeaders); - }), new TwigFilter('comment2', function ($value) { $value = explode("\n", $value); foreach ($value as $key => $line) { diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 952732fd96..71d1666cc1 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -392,7 +392,7 @@ type ImpersonationTarget = type ImpersonationClientParams = SessionClientParams & ImpersonationTarget; -{%~ set webHeaders = spec.global.headers | webClientHeaders %} +{%~ set webHeaders = spec.global.headers %} type LegacyClientSetter = Extract, `set${string}`>; type ClientAuthBuilder = Extract, `with${string}`>; type ClientInternalMethod = LegacyClientSetter | ClientAuthBuilder; @@ -528,7 +528,6 @@ class ClientRuntime { withForwardedUserAgent(forwardedUserAgent: string): this { this.headers['X-Forwarded-User-Agent'] = forwardedUserAgent; - this.config.forwardeduseragent = forwardedUserAgent; return this; } From 5c7b728d592d230b387210589fc5e6e12ff7588d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 6 May 2026 15:56:50 +0530 Subject: [PATCH 30/69] Generate web client base params from headers --- src/SDK/Language/Web.php | 39 ++++++++++++++ templates/web/src/client.ts.twig | 87 ++++++++++---------------------- 2 files changed, 67 insertions(+), 59 deletions(-) diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index ff53ddb647..454c864627 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -568,6 +568,42 @@ public function webMethodThisGate(array $method, array $service): string 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 getFilters(): array { return \array_merge(parent::getFilters(), [ @@ -634,6 +670,9 @@ public function getFilters(): array new TwigFilter('webMethodThisGate', function (array $method, array $service) { return $this->webMethodThisGate($method, $service); }, ['is_safe' => ['html']]), + new TwigFilter('webClientBaseParams', function (array $headers) { + return $this->webClientBaseParams($headers); + }), new TwigFilter('comment2', function ($value) { $value = explode("\n", $value); foreach ($value as $key => $line) { diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 71d1666cc1..140b5a91ca 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -355,34 +355,11 @@ type Prettify = { [K in keyof T]: T[K] } & {}; type BaseClientParams = { endpoint: string; - projectId: string; endpointRealtime?: string; - locale?: string; selfSigned?: boolean; -{%~ if sdk.platform == 'console' %} - mode?: string; - platform?: string; -{%~ endif %} -}; - -type SessionClientParams = BaseClientParams & { - session: string; -}; - -type ApiKeyClientParams = BaseClientParams & { - apiKey: string; -}; - -type CookieClientParams = BaseClientParams & { - cookie: string; -}; - -type JWTClientParams = BaseClientParams & { - jwt: string; -}; - -type DevKeyClientParams = BaseClientParams & { - devKey: string; +{%~ for param in spec.global.headers | webClientBaseParams %} + {{ param.name }}{% if not param.required %}?{% endif %}: string; +{%~ endfor %} }; type ImpersonationTarget = @@ -390,9 +367,6 @@ type ImpersonationTarget = | { email: string; userId?: never; phone?: never } | { phone: string; userId?: never; email?: never }; -type ImpersonationClientParams = SessionClientParams & ImpersonationTarget; - -{%~ set webHeaders = spec.global.headers %} type LegacyClientSetter = Extract, `set${string}`>; type ClientAuthBuilder = Extract, `with${string}`>; type ClientInternalMethod = LegacyClientSetter | ClientAuthBuilder; @@ -402,16 +376,16 @@ type LegacyClient = Omit, C type ClientConstructor = { new (): LegacyClient; fromBrowser(params: Prettify): Client<'browser'>; - fromSession(params: Prettify): Client<'session'>; + fromSession(params: Prettify): Client<'session'>; {%~ if sdk.platform == 'server' or sdk.platform == 'console' %} - fromAPIKey(params: Prettify): Client<'apiKey'>; + fromAPIKey(params: Prettify): Client<'apiKey'>; {%~ endif %} - fromCookie(params: Prettify): Client<'cookie'>; + fromCookie(params: Prettify): Client<'cookie'>; {%~ if sdk.platform == 'server' or sdk.platform == 'console' %} - fromJWT(params: Prettify): Client<'jwt'>; + fromJWT(params: Prettify): Client<'jwt'>; {%~ endif %} - fromDevKey(params: Prettify): Client<'devKey'>; - fromImpersonation(params: Prettify): Client<'impersonation'>; + fromDevKey(params: Prettify): Client<'devKey'>; + fromImpersonation(params: Prettify): Client<'impersonation'>; }; class ClientRuntime { @@ -424,14 +398,14 @@ class ClientRuntime { config: { endpoint: string; endpointRealtime: string; -{%~ for header in webHeaders %} +{%~ for header in spec.global.headers %} {{ header.key | caseLower }}: string; {%~ endfor %} selfSigned: boolean; } = { endpoint: '{{ spec.endpoint }}', endpointRealtime: '', -{%~ for header in webHeaders %} +{%~ for header in spec.global.headers %} {{ header.key | caseLower }}: '', {%~ endfor %} selfSigned: false, @@ -456,14 +430,14 @@ class ClientRuntime { return new ClientRuntime<'browser'>().applyBase<'browser'>(params, 'client'); } - static fromSession(params: Prettify): Client<'session'> { + static fromSession(params: Prettify): Client<'session'> { return new ClientRuntime<'session'>() .applyBase<'session'>(params, 'client') .setSession(params.session); } {%~ if sdk.platform == 'server' or sdk.platform == 'console' %} - static fromAPIKey(params: Prettify): Client<'apiKey'> { + static fromAPIKey(params: Prettify): Client<'apiKey'> { return new ClientRuntime<'apiKey'>() .applyBase<'apiKey'>(params, 'server') .setKey(params.apiKey); @@ -471,7 +445,7 @@ class ClientRuntime { {%~ endif %} - static fromCookie(params: Prettify): Client<'cookie'> { + static fromCookie(params: Prettify): Client<'cookie'> { return new ClientRuntime<'cookie'>() {%~ if sdk.platform == 'console' %} .applyBase<'cookie'>({ mode: 'admin', ...params }, 'server') @@ -482,7 +456,7 @@ class ClientRuntime { } {%~ if sdk.platform == 'server' or sdk.platform == 'console' %} - static fromJWT(params: Prettify): Client<'jwt'> { + static fromJWT(params: Prettify): Client<'jwt'> { return new ClientRuntime<'jwt'>() .applyBase<'jwt'>(params, 'server') .setJWT(params.jwt); @@ -490,13 +464,13 @@ class ClientRuntime { {%~ endif %} - static fromDevKey(params: Prettify): Client<'devKey'> { + static fromDevKey(params: Prettify): Client<'devKey'> { return new ClientRuntime<'devKey'>() .applyBase<'devKey'>(params, 'client') .setDevKey(params.devKey); } - static fromImpersonation(params: Prettify): Client<'impersonation'> { + static fromImpersonation(params: Prettify): Client<'impersonation'> { const targets = [ params.userId !== undefined, params.email !== undefined, @@ -536,30 +510,25 @@ class ClientRuntime { client.sdkPlatform = sdkPlatform; client.headers['x-sdk-platform'] = sdkPlatform === 'server' ? '{{ sdk.platform == 'console' ? 'console' : 'server' }}' : 'client'; client.setEndpoint(params.endpoint); - client.setProject(params.projectId); +{%~ for param in spec.global.headers | webClientBaseParams %} +{%~ if param.required %} + client.{{ param.setter }}(params.{{ param.name }}); - if (params.endpointRealtime !== undefined) { - client.setEndpointRealtime(params.endpointRealtime); +{%~ else %} + if (params.{{ param.name }} !== undefined) { + client.{{ param.setter }}(params.{{ param.name }}); } - if (params.locale !== undefined) { - client.setLocale(params.locale); +{%~ endif %} +{%~ endfor %} + if (params.endpointRealtime !== undefined) { + client.setEndpointRealtime(params.endpointRealtime); } if (params.selfSigned !== undefined) { client.setSelfSigned(params.selfSigned); } -{%~ if sdk.platform == 'console' %} - if (params.mode !== undefined) { - client.setMode(params.mode); - } - - if (params.platform !== undefined) { - client.setPlatform(params.platform); - } - -{%~ endif %} return client; } @@ -640,7 +609,7 @@ class ClientRuntime { return this; } -{%~ for header in webHeaders %} +{%~ for header in spec.global.headers %} /** * @deprecated Use a static client factory or factory params object instead. */ From 8427261df0f08a31fe5af8593fca06a4ba63b61f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 6 May 2026 15:58:58 +0530 Subject: [PATCH 31/69] Extract web client setter return types --- src/SDK/Language/Web.php | 16 ++++++++++++++++ templates/web/src/client.ts.twig | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index 454c864627..de2769f330 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -604,6 +604,19 @@ public function webClientBaseParams(array $headers): array 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(), [ @@ -673,6 +686,9 @@ public function getFilters(): array new TwigFilter('webClientBaseParams', function (array $headers) { return $this->webClientBaseParams($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) { diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 140b5a91ca..64fd2e5677 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -613,10 +613,10 @@ class ClientRuntime { /** * @deprecated Use a static client factory or factory params object instead. */ - set{{ header.key | caseUcfirst }}(value: string): {% if header.key == 'Key' %}ClientRuntime<'apiKey'>{% elseif header.key == 'Cookie' %}ClientRuntime<'cookie'>{% elseif header.key == 'JWT' %}ClientRuntime<'jwt'>{% elseif header.key == 'Session' %}ClientRuntime<'session'>{% elseif header.key == 'DevKey' %}ClientRuntime<'devKey'>{% elseif header.key in ['ImpersonateUserId', 'ImpersonateUserEmail', 'ImpersonateUserPhone'] %}ClientRuntime<'impersonation'>{% else %}this{% endif %} { + set{{ header.key | caseUcfirst }}(value: string): {{ header | webClientSetterReturnType }} { this.headers['{{ header.name }}'] = value; this.config.{{ header.key | caseLower }} = value; - return this as unknown as {% if header.key == 'Key' %}ClientRuntime<'apiKey'>{% elseif header.key == 'Cookie' %}ClientRuntime<'cookie'>{% elseif header.key == 'JWT' %}ClientRuntime<'jwt'>{% elseif header.key == 'Session' %}ClientRuntime<'session'>{% elseif header.key == 'DevKey' %}ClientRuntime<'devKey'>{% elseif header.key in ['ImpersonateUserId', 'ImpersonateUserEmail', 'ImpersonateUserPhone'] %}ClientRuntime<'impersonation'>{% else %}this{% endif %}; + return this as unknown as {{ header | webClientSetterReturnType }}; } {%~ endfor %} From 4cf290706fb7703a364de3ecc04f04613a31e79a Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 6 May 2026 16:08:53 +0530 Subject: [PATCH 32/69] Update web test auth headers fixture --- tests/resources/spec.json | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/resources/spec.json b/tests/resources/spec.json index 0ab4eacf20..9fa3299e62 100644 --- a/tests/resources/spec.json +++ b/tests/resources/spec.json @@ -59,6 +59,12 @@ "description": "The user session to authenticate with", "in": "header" }, + "Cookie": { + "type": "apiKey", + "name": "Cookie", + "description": "Cookie header", + "in": "header" + }, "Locale": { "type": "apiKey", "name": "X-Appwrite-Locale", @@ -68,6 +74,30 @@ "demo": "en" } }, + "DevKey": { + "type": "apiKey", + "name": "X-Appwrite-Dev-Key", + "description": "Your development key", + "in": "header" + }, + "ImpersonateUserId": { + "type": "apiKey", + "name": "X-Appwrite-Impersonate-User-Id", + "description": "User ID to impersonate", + "in": "header" + }, + "ImpersonateUserEmail": { + "type": "apiKey", + "name": "X-Appwrite-Impersonate-User-Email", + "description": "User email to impersonate", + "in": "header" + }, + "ImpersonateUserPhone": { + "type": "apiKey", + "name": "X-Appwrite-Impersonate-User-Phone", + "description": "User phone to impersonate", + "in": "header" + }, "Mode": { "type": "apiKey", "name": "X-Appwrite-Mode", From 0472fb194699e5a3218da084a1c9b79b46f91f1d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 6 May 2026 16:32:54 +0530 Subject: [PATCH 33/69] Add Flutter client factory surface --- src/SDK/Language/Flutter.php | 2 +- templates/flutter/base/requests/api.twig | 2 +- templates/flutter/base/requests/file.twig | 2 +- templates/flutter/base/requests/location.twig | 4 +- templates/flutter/base/requests/oauth.twig | 6 +- templates/flutter/lib/package.dart.twig | 3 +- .../flutter/lib/services/service.dart.twig | 7 +- templates/flutter/lib/src/client.dart.twig | 185 +++++++++++++++--- .../flutter/lib/src/client_base.dart.twig | 4 + .../flutter/lib/src/client_browser.dart.twig | 4 + templates/flutter/lib/src/client_io.dart.twig | 4 + templates/flutter/lib/src/realtime.dart.twig | 2 +- .../lib/src/realtime_browser.dart.twig | 4 +- .../flutter/lib/src/realtime_io.dart.twig | 4 +- .../flutter/lib/src/realtime_mixin.dart.twig | 4 +- .../flutter/lib/src/realtime_stub.dart.twig | 2 +- templates/flutter/lib/src/service.dart.twig | 5 + 17 files changed, 199 insertions(+), 45 deletions(-) create mode 100644 templates/flutter/lib/src/service.dart.twig diff --git a/src/SDK/Language/Flutter.php b/src/SDK/Language/Flutter.php index a17254a332..646b9e996f 100644 --- a/src/SDK/Language/Flutter.php +++ b/src/SDK/Language/Flutter.php @@ -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', diff --git a/templates/flutter/base/requests/api.twig b/templates/flutter/base/requests/api.twig index 0bccf37378..07305df90e 100644 --- a/templates/flutter/base/requests/api.twig +++ b/templates/flutter/base/requests/api.twig @@ -8,7 +8,7 @@ {{~ utils.map_headers(method.headers) }} }; - final res = await client.call(HttpMethod.{{ method.method | caseLower }}, path: apiPath, params: apiParams, headers: apiHeaders); + final res = await _client.call(HttpMethod.{{ method.method | caseLower }}, path: apiPath, params: apiParams, headers: apiHeaders); {% set responseModels = method | getValidResponseModels %} {% set hasResponseDiscriminator = method.responseDiscriminator is defined and method.responseDiscriminator is not empty %} diff --git a/templates/flutter/base/requests/file.twig b/templates/flutter/base/requests/file.twig index f561b7c0e4..198f103248 100644 --- a/templates/flutter/base/requests/file.twig +++ b/templates/flutter/base/requests/file.twig @@ -18,7 +18,7 @@ idParamName = '{{ parameter.name }}'; {% endif %} {% endfor %} - final res = await client.chunkedUpload( + final res = await _client.chunkedUpload( path: apiPath, params: apiParams, paramName: paramName, diff --git a/templates/flutter/base/requests/location.twig b/templates/flutter/base/requests/location.twig index 1135c3cad5..c44253debf 100644 --- a/templates/flutter/base/requests/location.twig +++ b/templates/flutter/base/requests/location.twig @@ -4,11 +4,11 @@ {{ utils.map_parameter(method.parameters.body) }} {% if method.auth|length > 0 %}{% for node in method.auth %} {% for key,header in node|keys %} - '{{header|caseLower}}': client.config['{{header|caseLower}}'], + '{{header|caseLower}}': _client.config['{{header|caseLower}}'], {% endfor %} {% endfor %} {% endif %} }; - final res = await client.call(HttpMethod.{{ method.method | caseLower }}, path: apiPath, params: params, responseType: ResponseType.bytes); + final res = await _client.call(HttpMethod.{{ method.method | caseLower }}, path: apiPath, params: params, responseType: ResponseType.bytes); return res.data; \ No newline at end of file diff --git a/templates/flutter/base/requests/oauth.twig b/templates/flutter/base/requests/oauth.twig index ee1c182013..70a77713db 100644 --- a/templates/flutter/base/requests/oauth.twig +++ b/templates/flutter/base/requests/oauth.twig @@ -5,7 +5,7 @@ {% if method.auth|length > 0 %} {% for node in method.auth %} {% for key,header in node|keys %} - '{{header|caseLower}}': client.config['{{header|caseLower}}'], + '{{header|caseLower}}': _client.config['{{header|caseLower}}'], {% endfor %} {% endfor %} {% endif %} @@ -24,7 +24,7 @@ } }); - Uri endpoint = Uri.parse(client.endPoint); + Uri endpoint = Uri.parse(_client.endPoint); Uri url = Uri(scheme: endpoint.scheme, host: endpoint.host, port: endpoint.port, @@ -32,4 +32,4 @@ query: query.join('&') ); - return client.webAuth(url, callbackUrlScheme: success); \ No newline at end of file + return _client.webAuth(url, callbackUrlScheme: success); \ No newline at end of file diff --git a/templates/flutter/lib/package.dart.twig b/templates/flutter/lib/package.dart.twig index 515452fa64..51e0b3dd9b 100644 --- a/templates/flutter/lib/package.dart.twig +++ b/templates/flutter/lib/package.dart.twig @@ -12,6 +12,7 @@ import 'dart:convert'; import 'src/enums.dart'; import 'src/service.dart'; +import 'src/client.dart'; import 'src/input_file.dart'; import 'models.dart' as models; import 'enums.dart' as enums; @@ -34,4 +35,4 @@ part 'channel.dart'; part 'operator.dart'; {% for service in spec.services %} part 'services/{{service.name | caseSnake}}.dart'; -{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/templates/flutter/lib/services/service.dart.twig b/templates/flutter/lib/services/service.dart.twig index ad99577622..a6f55f4596 100644 --- a/templates/flutter/lib/services/service.dart.twig +++ b/templates/flutter/lib/services/service.dart.twig @@ -11,8 +11,13 @@ part of '../{{ language.params.packageName }}.dart'; {{- service.description|dartComment | split(' ///') | join('///')}} {% endif %} class {{ service.name | caseUcfirst }} extends Service { + final ClientAuth _client; + /// Initializes a [{{ service.name | caseUcfirst }}] service - {{ service.name | caseUcfirst }}(super.client); + // ignore: use_super_parameters + {{ service.name | caseUcfirst }}(ClientAuth client) + : _client = client, + super(client); {% for method in service.methods %} {%~ if method.description %} diff --git a/templates/flutter/lib/src/client.dart.twig b/templates/flutter/lib/src/client.dart.twig index 6c2b6be796..f1301df7fd 100644 --- a/templates/flutter/lib/src/client.dart.twig +++ b/templates/flutter/lib/src/client.dart.twig @@ -8,26 +8,15 @@ import 'upload_progress.dart'; /// [Client] that handles requests to {{spec.title | caseUcfirst}}. /// /// The [Client] is also responsible for managing user's sessions. -abstract class Client { - /// The size for chunked uploads in bytes. - static const int chunkSize = 5 * 1024 * 1024; - +abstract class ClientAuth { /// Holds configuration such as project. - late Map config; - late String _endPoint; - late String? _endPointRealtime; + Map get config; /// {{spec.title | caseUcfirst}} endpoint. - String get endPoint => _endPoint; + String get endPoint; /// {{spec.title | caseUcfirst}} realtime endpoint. - String? get endPointRealtime => _endPointRealtime; - - /// Initializes a [Client]. - factory Client({ - String endPoint = '{{ spec.endpoint }}', - bool selfSigned = false, - }) => createClient(endPoint: endPoint, selfSigned: selfSigned); + String? get endPointRealtime; /// Handle OAuth2 session creation. Future webAuth(Uri url, {String? callbackUrlScheme}); @@ -42,17 +31,170 @@ abstract class Client { Function(UploadProgress)? onProgress, }); + /// Sends a "ping" request to Appwrite to verify connectivity. + Future ping(); + + /// Send the API request. + Future call( + HttpMethod method, { + String path = '', + Map headers = const {}, + Map params = const {}, + ResponseType? responseType, + }); +} + +/// Legacy client with mutable setter methods. +abstract class Client implements ClientAuth { + /// The size for chunked uploads in bytes. + static const int chunkSize = 5 * 1024 * 1024; + + /// Initializes a [Client]. + factory Client({ + String endPoint = '{{ spec.endpoint }}', + bool selfSigned = false, + }) => createClient(endPoint: endPoint, selfSigned: selfSigned); + + /// Initializes a client for browser/mobile session authentication. + static ClientAuth fromBrowser({ + String endPoint = '{{ spec.endpoint }}', + required String projectId, + String? endPointRealtime, + String? locale, + bool selfSigned = false, + }) => _fromBrowserClient( + endPoint: endPoint, + projectId: projectId, + endPointRealtime: endPointRealtime, + locale: locale, + selfSigned: selfSigned, + ); + + static Client _fromBrowserClient({ + required String endPoint, + required String projectId, + String? endPointRealtime, + String? locale, + required bool selfSigned, + }) { + final client = createClient(endPoint: endPoint, selfSigned: selfSigned); + + // ignore: deprecated_member_use_from_same_package + client.setProject(projectId); + if (endPointRealtime != null) { + // ignore: deprecated_member_use_from_same_package + client.setEndPointRealtime(endPointRealtime); + } + if (locale != null) { + // ignore: deprecated_member_use_from_same_package + client.setLocale(locale); + } + + return client; + } + + /// Initializes a client with a user session. + static ClientAuth fromSession({ + String endPoint = '{{ spec.endpoint }}', + required String projectId, + required String session, + String? endPointRealtime, + String? locale, + bool selfSigned = false, + }) { + final client = _fromBrowserClient( + endPoint: endPoint, + projectId: projectId, + endPointRealtime: endPointRealtime, + locale: locale, + selfSigned: selfSigned, + ); + + // ignore: deprecated_member_use_from_same_package + client.setSession(session); + return client; + } + + /// Initializes a client with a development key. + static ClientAuth fromDevKey({ + String endPoint = '{{ spec.endpoint }}', + required String projectId, + required String devKey, + String? endPointRealtime, + String? locale, + bool selfSigned = false, + }) { + final client = _fromBrowserClient( + endPoint: endPoint, + projectId: projectId, + endPointRealtime: endPointRealtime, + locale: locale, + selfSigned: selfSigned, + ); + + // ignore: deprecated_member_use_from_same_package + client.setDevKey(devKey); + return client; + } + + /// Initializes a client with user impersonation. + static ClientAuth fromImpersonation({ + String endPoint = '{{ spec.endpoint }}', + required String projectId, + required String session, + String? userId, + String? userEmail, + String? userPhone, + String? endPointRealtime, + String? locale, + bool selfSigned = false, + }) { + final targets = [userId, userEmail, userPhone].whereType().length; + if (targets != 1) { + throw ArgumentError( + 'Exactly one of userId, userEmail, or userPhone must be provided.', + ); + } + + final client = _fromBrowserClient( + endPoint: endPoint, + projectId: projectId, + endPointRealtime: endPointRealtime, + locale: locale, + selfSigned: selfSigned, + ); + + // ignore: deprecated_member_use_from_same_package + client.setSession(session); + + if (userId != null) { + // ignore: deprecated_member_use_from_same_package + client.setImpersonateUserId(userId); + } else if (userEmail != null) { + // ignore: deprecated_member_use_from_same_package + client.setImpersonateUserEmail(userEmail); + } else if (userPhone != null) { + // ignore: deprecated_member_use_from_same_package + client.setImpersonateUserPhone(userPhone); + } + + return client; + } + /// Set self signed to [status]. /// /// If self signed is true, [Client] will ignore invalid certificates. /// This is helpful in environments where your {{spec.title | caseUcfirst}} /// instance does not have a valid SSL certificate. + @Deprecated('Use Client.fromBrowser or another factory constructor instead.') Client setSelfSigned({bool status = true}); /// Set the {{spec.title | caseUcfirst}} endpoint. + @Deprecated('Use Client.fromBrowser or another factory constructor instead.') Client setEndpoint(String endPoint); /// Set the {{spec.title | caseUcfirst}} realtime endpoint. + @Deprecated('Use Client.fromBrowser or another factory constructor instead.') Client setEndPointRealtime(String endPoint); {% for header in spec.global.headers %} @@ -61,6 +203,7 @@ abstract class Client { /// /// {{header.description}}. {% endif %} + @Deprecated('Use Client.fromBrowser or another factory constructor instead.') Client set{{header.key | caseUcfirst}}(String value); {% endfor %} @@ -69,16 +212,4 @@ abstract class Client { /// Get the current request headers. Map getHeaders(); - - /// Sends a "ping" request to Appwrite to verify connectivity. - Future ping(); - - /// Send the API request. - Future call( - HttpMethod method, { - String path = '', - Map headers = const {}, - Map params = const {}, - ResponseType? responseType, - }); } diff --git a/templates/flutter/lib/src/client_base.dart.twig b/templates/flutter/lib/src/client_base.dart.twig index 1eb042197c..c9bf944c84 100644 --- a/templates/flutter/lib/src/client_base.dart.twig +++ b/templates/flutter/lib/src/client_base.dart.twig @@ -7,16 +7,20 @@ abstract class ClientBase implements Client { {% if header.description %} /// {{header.description}} {% endif %} + @Deprecated('Use Client.fromBrowser or another factory constructor instead.') @override ClientBase set{{header.key | caseUcfirst}}(value); {% endfor %} + @Deprecated('Use Client.fromBrowser or another factory constructor instead.') @override ClientBase setSelfSigned({bool status = true}); + @Deprecated('Use Client.fromBrowser or another factory constructor instead.') @override ClientBase setEndpoint(String endPoint); + @Deprecated('Use Client.fromBrowser or another factory constructor instead.') @override Client setEndPointRealtime(String endPoint); diff --git a/templates/flutter/lib/src/client_browser.dart.twig b/templates/flutter/lib/src/client_browser.dart.twig index 7ad50dea9b..822057957c 100644 --- a/templates/flutter/lib/src/client_browser.dart.twig +++ b/templates/flutter/lib/src/client_browser.dart.twig @@ -62,6 +62,7 @@ class ClientBrowser extends ClientBase with ClientMixin { {% if header.description %} /// {{header.description}} {% endif %} + @Deprecated('Use Client.fromBrowser or another factory constructor instead.') @override ClientBrowser set{{header.key | caseUcfirst}}(value) { config['{{ header.key | caseCamel }}'] = value; @@ -70,11 +71,13 @@ class ClientBrowser extends ClientBase with ClientMixin { } {% endfor %} + @Deprecated('Use Client.fromBrowser or another factory constructor instead.') @override ClientBrowser setSelfSigned({bool status = true}) { return this; } + @Deprecated('Use Client.fromBrowser or another factory constructor instead.') @override ClientBrowser setEndpoint(String endPoint) { if (!endPoint.startsWith('http://') && !endPoint.startsWith('https://')) { @@ -89,6 +92,7 @@ class ClientBrowser extends ClientBase with ClientMixin { return this; } + @Deprecated('Use Client.fromBrowser or another factory constructor instead.') @override ClientBrowser setEndPointRealtime(String endPoint) { if (!endPoint.startsWith('ws://') && !endPoint.startsWith('wss://')) { diff --git a/templates/flutter/lib/src/client_io.dart.twig b/templates/flutter/lib/src/client_io.dart.twig index 14ea8bfd62..e5fa2e7bae 100644 --- a/templates/flutter/lib/src/client_io.dart.twig +++ b/templates/flutter/lib/src/client_io.dart.twig @@ -88,6 +88,7 @@ class ClientIO extends ClientBase with ClientMixin { {% if header.description %} /// {{header.description}} {% endif %} + @Deprecated('Use Client.fromBrowser or another factory constructor instead.') @override ClientIO set{{header.key | caseUcfirst}}(value) { config['{{ header.key | caseCamel }}'] = value; @@ -96,6 +97,7 @@ class ClientIO extends ClientBase with ClientMixin { } {% endfor %} + @Deprecated('Use Client.fromBrowser or another factory constructor instead.') @override ClientIO setSelfSigned({bool status = true}) { selfSigned = status; @@ -104,6 +106,7 @@ class ClientIO extends ClientBase with ClientMixin { return this; } + @Deprecated('Use Client.fromBrowser or another factory constructor instead.') @override ClientIO setEndpoint(String endPoint) { if (!endPoint.startsWith('http://') && !endPoint.startsWith('https://')) { @@ -118,6 +121,7 @@ class ClientIO extends ClientBase with ClientMixin { return this; } + @Deprecated('Use Client.fromBrowser or another factory constructor instead.') @override ClientIO setEndPointRealtime(String endPoint) { if (!endPoint.startsWith('ws://') && !endPoint.startsWith('wss://')) { diff --git a/templates/flutter/lib/src/realtime.dart.twig b/templates/flutter/lib/src/realtime.dart.twig index 2126620288..f371166962 100644 --- a/templates/flutter/lib/src/realtime.dart.twig +++ b/templates/flutter/lib/src/realtime.dart.twig @@ -8,7 +8,7 @@ import 'client.dart'; /// Realtime allows you to listen to any events on the server-side in realtime using the subscribe method. abstract class Realtime extends Service { /// Initializes a [Realtime] service - factory Realtime(Client client) => createRealtime(client); + factory Realtime(ClientAuth client) => createRealtime(client); /// Subscribes to Appwrite events and returns a `RealtimeSubscription` object, which can be used /// to listen to events on the channels in realtime and to close the subscription to stop listening. diff --git a/templates/flutter/lib/src/realtime_browser.dart.twig b/templates/flutter/lib/src/realtime_browser.dart.twig index 8ae3c74033..ef20bf23e7 100644 --- a/templates/flutter/lib/src/realtime_browser.dart.twig +++ b/templates/flutter/lib/src/realtime_browser.dart.twig @@ -9,12 +9,12 @@ import 'client.dart'; import 'client_browser.dart'; import 'realtime_mixin.dart'; -RealtimeBase createRealtime(Client client) => RealtimeBrowser(client); +RealtimeBase createRealtime(ClientAuth client) => RealtimeBrowser(client); class RealtimeBrowser extends RealtimeBase with RealtimeMixin { Map? lastMessage; - RealtimeBrowser(Client client) { + RealtimeBrowser(ClientAuth client) { this.client = client; getWebSocket = _getWebSocket; getFallbackCookie = _getFallbackCookie; diff --git a/templates/flutter/lib/src/realtime_io.dart.twig b/templates/flutter/lib/src/realtime_io.dart.twig index 1735580b26..0d673fcea3 100644 --- a/templates/flutter/lib/src/realtime_io.dart.twig +++ b/templates/flutter/lib/src/realtime_io.dart.twig @@ -12,11 +12,11 @@ import 'realtime_mixin.dart'; import 'client.dart'; import 'client_io.dart'; -RealtimeBase createRealtime(Client client) => RealtimeIO(client); +RealtimeBase createRealtime(ClientAuth client) => RealtimeIO(client); class RealtimeIO extends RealtimeBase with RealtimeMixin { - RealtimeIO(Client client) { + RealtimeIO(ClientAuth client) { this.client = client; getWebSocket = _getWebSocket; } diff --git a/templates/flutter/lib/src/realtime_mixin.dart.twig b/templates/flutter/lib/src/realtime_mixin.dart.twig index 4eab921346..1caf44fc79 100644 --- a/templates/flutter/lib/src/realtime_mixin.dart.twig +++ b/templates/flutter/lib/src/realtime_mixin.dart.twig @@ -27,7 +27,7 @@ String _uniqueSubscriptionId() { } mixin RealtimeMixin { - late Client client; + late ClientAuth client; final Map _subscriptions = {}; final Map> _pendingSubscribes = {}; WebSocketChannel? _websok; @@ -359,4 +359,4 @@ mixin RealtimeMixin { _retry(); } } -} \ No newline at end of file +} diff --git a/templates/flutter/lib/src/realtime_stub.dart.twig b/templates/flutter/lib/src/realtime_stub.dart.twig index ce0b7e954b..0688a7246a 100644 --- a/templates/flutter/lib/src/realtime_stub.dart.twig +++ b/templates/flutter/lib/src/realtime_stub.dart.twig @@ -2,6 +2,6 @@ import 'realtime_base.dart'; import 'client.dart'; /// Implemented in `realtime_browser.dart` and `realtime_io.dart`. -RealtimeBase createRealtime(Client client) => throw UnsupportedError( +RealtimeBase createRealtime(ClientAuth client) => throw UnsupportedError( 'Cannot create a client without dart:html or dart:io.', ); diff --git a/templates/flutter/lib/src/service.dart.twig b/templates/flutter/lib/src/service.dart.twig new file mode 100644 index 0000000000..3df1be6814 --- /dev/null +++ b/templates/flutter/lib/src/service.dart.twig @@ -0,0 +1,5 @@ +import 'client.dart'; + +class Service { + const Service(ClientAuth client); +} From 5ae2b1c0917141d3d94f076160c4e2b8905a9004 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 6 May 2026 16:44:48 +0530 Subject: [PATCH 34/69] Make Flutter factories independent of optional setters --- templates/flutter/lib/src/client.dart.twig | 74 +++++++++++++++++----- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/templates/flutter/lib/src/client.dart.twig b/templates/flutter/lib/src/client.dart.twig index f1301df7fd..31b4516034 100644 --- a/templates/flutter/lib/src/client.dart.twig +++ b/templates/flutter/lib/src/client.dart.twig @@ -79,20 +79,38 @@ abstract class Client implements ClientAuth { }) { final client = createClient(endPoint: endPoint, selfSigned: selfSigned); - // ignore: deprecated_member_use_from_same_package - client.setProject(projectId); + _setHeader( + client, + 'project', + 'X-{{ spec.title | caseUcfirst }}-Project', + projectId, + ); if (endPointRealtime != null) { // ignore: deprecated_member_use_from_same_package client.setEndPointRealtime(endPointRealtime); } if (locale != null) { - // ignore: deprecated_member_use_from_same_package - client.setLocale(locale); + _setHeader( + client, + 'locale', + 'X-{{ spec.title | caseUcfirst }}-Locale', + locale, + ); } return client; } + static void _setHeader( + Client client, + String configKey, + String header, + String value, + ) { + client.config[configKey] = value; + client.addHeader(header, value); + } + /// Initializes a client with a user session. static ClientAuth fromSession({ String endPoint = '{{ spec.endpoint }}', @@ -110,8 +128,12 @@ abstract class Client implements ClientAuth { selfSigned: selfSigned, ); - // ignore: deprecated_member_use_from_same_package - client.setSession(session); + _setHeader( + client, + 'session', + 'X-{{ spec.title | caseUcfirst }}-Session', + session, + ); return client; } @@ -132,8 +154,12 @@ abstract class Client implements ClientAuth { selfSigned: selfSigned, ); - // ignore: deprecated_member_use_from_same_package - client.setDevKey(devKey); + _setHeader( + client, + 'devKey', + 'X-{{ spec.title | caseUcfirst }}-Dev-Key', + devKey, + ); return client; } @@ -164,18 +190,34 @@ abstract class Client implements ClientAuth { selfSigned: selfSigned, ); - // ignore: deprecated_member_use_from_same_package - client.setSession(session); + _setHeader( + client, + 'session', + 'X-{{ spec.title | caseUcfirst }}-Session', + session, + ); if (userId != null) { - // ignore: deprecated_member_use_from_same_package - client.setImpersonateUserId(userId); + _setHeader( + client, + 'impersonateUserId', + 'X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id', + userId, + ); } else if (userEmail != null) { - // ignore: deprecated_member_use_from_same_package - client.setImpersonateUserEmail(userEmail); + _setHeader( + client, + 'impersonateUserEmail', + 'X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email', + userEmail, + ); } else if (userPhone != null) { - // ignore: deprecated_member_use_from_same_package - client.setImpersonateUserPhone(userPhone); + _setHeader( + client, + 'impersonateUserPhone', + 'X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone', + userPhone, + ); } return client; From 7e1e7bfd475c3e4a90220b83108b029b6be3fec5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 6 May 2026 16:48:53 +0530 Subject: [PATCH 35/69] Address Flutter client surface review comments --- templates/flutter/lib/src/realtime_browser.dart.twig | 8 ++++++++ templates/flutter/lib/src/realtime_io.dart.twig | 8 ++++++++ templates/flutter/lib/src/service.dart.twig | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/templates/flutter/lib/src/realtime_browser.dart.twig b/templates/flutter/lib/src/realtime_browser.dart.twig index ef20bf23e7..75417180bb 100644 --- a/templates/flutter/lib/src/realtime_browser.dart.twig +++ b/templates/flutter/lib/src/realtime_browser.dart.twig @@ -15,6 +15,14 @@ class RealtimeBrowser extends RealtimeBase with RealtimeMixin { Map? lastMessage; RealtimeBrowser(ClientAuth client) { + if (client is! ClientBrowser) { + throw ArgumentError.value( + client, + 'client', + 'RealtimeBrowser requires a ClientBrowser instance.', + ); + } + this.client = client; getWebSocket = _getWebSocket; getFallbackCookie = _getFallbackCookie; diff --git a/templates/flutter/lib/src/realtime_io.dart.twig b/templates/flutter/lib/src/realtime_io.dart.twig index 0d673fcea3..d5739906f3 100644 --- a/templates/flutter/lib/src/realtime_io.dart.twig +++ b/templates/flutter/lib/src/realtime_io.dart.twig @@ -17,6 +17,14 @@ RealtimeBase createRealtime(ClientAuth client) => RealtimeIO(client); class RealtimeIO extends RealtimeBase with RealtimeMixin { RealtimeIO(ClientAuth client) { + if (client is! ClientIO) { + throw ArgumentError.value( + client, + 'client', + 'RealtimeIO requires a ClientIO instance.', + ); + } + this.client = client; getWebSocket = _getWebSocket; } diff --git a/templates/flutter/lib/src/service.dart.twig b/templates/flutter/lib/src/service.dart.twig index 3df1be6814..ca2c0d57fc 100644 --- a/templates/flutter/lib/src/service.dart.twig +++ b/templates/flutter/lib/src/service.dart.twig @@ -1,5 +1,5 @@ import 'client.dart'; class Service { - const Service(ClientAuth client); + const Service(ClientAuth _); } From 8c6d089c0adfbc1aea156dda88d5e4cd782a7b11 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 6 May 2026 17:09:00 +0530 Subject: [PATCH 36/69] Add Apple client auth factories --- src/SDK/Language/Apple.php | 2 +- templates/apple/Sources/Client.swift.twig | 255 ++++++++++++++++-- templates/apple/Sources/Service.swift.twig | 6 + .../Sources/Services/Realtime.swift.twig | 10 +- .../apple/Sources/Services/Service.swift.twig | 11 +- templates/apple/base/params.twig | 27 ++ templates/apple/base/requests/oauth.twig | 2 +- 7 files changed, 289 insertions(+), 24 deletions(-) create mode 100644 templates/apple/Sources/Service.swift.twig create mode 100644 templates/apple/base/params.twig diff --git a/src/SDK/Language/Apple.php b/src/SDK/Language/Apple.php index 108598369f..f4bc4c2083 100644 --- a/src/SDK/Language/Apple.php +++ b/src/SDK/Language/Apple.php @@ -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', diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index 27a76f9704..51a03fc6d7 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -12,7 +12,72 @@ import AsyncHTTPClient let DASHDASH = "--" let CRLF = "\r\n" -open class Client { +public protocol ClientAuth: AnyObject { + var endPoint: String { get } + var endPointRealtime: String? { get } + var selfSigned: Bool { get } + + func getConfig(key: String) -> String? + func parametersToQueryString(params: [String: Any?]) -> String + func ping() async throws -> String + func call( + method: String, + path: String, + headers: [String: String], + params: [String: Any?], + sink: ((ByteBuffer) -> Void)?, + converter: ((Any) throws -> T)? + ) async throws -> T + func chunkedUpload( + path: String, + headers: inout [String: String], + params: inout [String: Any?], + paramName: String, + idParamName: String?, + converter: ((Any) throws -> T)?, + onProgress: ((UploadProgress) -> Void)? + ) async throws -> T +} + +public extension ClientAuth { + func call( + method: String, + path: String = "", + headers: [String: String] = [:], + params: [String: Any?] = [:], + converter: ((Any) throws -> T)? = nil + ) async throws -> T { + return try await call( + method: method, + path: path, + headers: headers, + params: params, + sink: nil, + converter: converter + ) + } + + func chunkedUpload( + path: String, + headers: inout [String: String], + params: inout [String: Any?], + paramName: String, + idParamName: String?, + onProgress: ((UploadProgress) -> Void)? = nil + ) async throws -> T { + return try await chunkedUpload( + path: path, + headers: &headers, + params: ¶ms, + paramName: paramName, + idParamName: idParamName, + converter: nil, + onProgress: onProgress + ) + } +} + +open class Client: ClientAuth { // MARK: Properties public static var chunkSize = 5 * 1024 * 1024 // 5MB @@ -36,7 +101,7 @@ open class Client { internal var config: [String: String] = [:] - internal var selfSigned: Bool = false + public internal(set) var selfSigned: Bool = false internal var compression: Bool = true @@ -58,6 +123,162 @@ open class Client { addOriginHeader() } + public static func fromBrowser( + endpoint: String = "{{spec.endpoint}}", + projectId: String, + endpointRealtime: String? = nil, + locale: String? = nil, + selfSigned: Bool = false + ) -> ClientAuth { + let client = Client() + configure( + client, + endpoint: endpoint, + endpointRealtime: endpointRealtime, + projectId: projectId, + locale: locale, + selfSigned: selfSigned + ) + return client + } + + public static func fromSession( + endpoint: String = "{{spec.endpoint}}", + projectId: String, + session: String, + endpointRealtime: String? = nil, + locale: String? = nil, + selfSigned: Bool = false + ) -> ClientAuth { + let client = Client() + configure( + client, + endpoint: endpoint, + endpointRealtime: endpointRealtime, + projectId: projectId, + locale: locale, + selfSigned: selfSigned + ) + setHeader(client, configKey: "session", header: "X-Appwrite-Session", value: session) + return client + } + + public static func fromDevKey( + endpoint: String = "{{spec.endpoint}}", + projectId: String, + devKey: String, + endpointRealtime: String? = nil, + locale: String? = nil, + selfSigned: Bool = false + ) -> ClientAuth { + let client = Client() + configure( + client, + endpoint: endpoint, + endpointRealtime: endpointRealtime, + projectId: projectId, + locale: locale, + selfSigned: selfSigned + ) + setHeader(client, configKey: "devkey", header: "X-Appwrite-Dev-Key", value: devKey) + return client + } + + public static func fromImpersonation( + endpoint: String = "{{spec.endpoint}}", + projectId: String, + session: String, + userId: String? = nil, + userEmail: String? = nil, + userPhone: String? = nil, + endpointRealtime: String? = nil, + locale: String? = nil, + selfSigned: Bool = false + ) -> ClientAuth { + let targetCount = [userId, userEmail, userPhone].compactMap { $0 }.count + precondition(targetCount == 1, "Provide exactly one impersonation target.") + + let client = Client() + configure( + client, + endpoint: endpoint, + endpointRealtime: endpointRealtime, + projectId: projectId, + locale: locale, + selfSigned: selfSigned + ) + setHeader(client, configKey: "session", header: "X-Appwrite-Session", value: session) + + if let userId = userId { + setHeader(client, configKey: "impersonateuserid", header: "X-Appwrite-Impersonate-User-Id", value: userId) + } + + if let userEmail = userEmail { + setHeader(client, configKey: "impersonateuseremail", header: "X-Appwrite-Impersonate-User-Email", value: userEmail) + } + + if let userPhone = userPhone { + setHeader(client, configKey: "impersonateuserphone", header: "X-Appwrite-Impersonate-User-Phone", value: userPhone) + } + + return client + } + + private static func configure( + _ client: Client, + endpoint: String, + endpointRealtime: String?, + projectId: String, + locale: String?, + selfSigned: Bool + ) { + client.configureEndpoint(endpoint) + + if let endpointRealtime = endpointRealtime { + client.configureEndpointRealtime(endpointRealtime) + } + + if selfSigned { + client.configureSelfSigned(selfSigned) + } + + setHeader(client, configKey: "project", header: "X-Appwrite-Project", value: projectId) + + if let locale = locale { + setHeader(client, configKey: "locale", header: "X-Appwrite-Locale", value: locale) + } + } + + private static func setHeader(_ client: Client, configKey: String, header: String, value: String) { + client.config[configKey] = value + _ = client.addHeader(key: header, value: value) + } + + private func configureSelfSigned(_ status: Bool) { + self.selfSigned = status + try! http.syncShutdown() + http = Client.createHTTP(selfSigned: status, compression: compression) + } + + private func configureEndpoint(_ endPoint: String) { + if !endPoint.hasPrefix("http://") && !endPoint.hasPrefix("https://") { + fatalError("Invalid endpoint URL: \(endPoint)") + } + + self.endPoint = endPoint + self.endPointRealtime = endPoint + .replacingOccurrences(of: "http://", with: "ws://") + .replacingOccurrences(of: "https://", with: "wss://") + } + + private func configureEndpointRealtime(_ endPoint: String) { + if !endPoint.hasPrefix("ws://") && !endPoint.hasPrefix("wss://") { + fatalError("Invalid realtime endpoint URL: \(endPoint)") + } + + self.endPointRealtime = endPoint + } + private static func createHTTP( selfSigned: Bool = false, compression: Bool = true, @@ -112,6 +333,7 @@ open class Client { /// /// @return Client /// + @available(*, deprecated, message: "Use Client.fromBrowser, Client.fromSession, Client.fromDevKey, or Client.fromImpersonation instead.") open func set{{ header.key | caseUcfirst }}(_ value: String) -> Client { config["{{ header.key | caseLower }}"] = value _ = addHeader(key: "{{header.name}}", value: value) @@ -127,10 +349,9 @@ open class Client { /// /// @return Client /// + @available(*, deprecated, message: "Use Client.fromBrowser, Client.fromSession, Client.fromDevKey, or Client.fromImpersonation instead.") open func setSelfSigned(_ status: Bool = true) -> Client { - self.selfSigned = status - try! http.syncShutdown() - http = Client.createHTTP(selfSigned: status, compression: compression) + configureSelfSigned(status) return self } @@ -171,16 +392,9 @@ open class Client { /// /// @return Client /// + @available(*, deprecated, message: "Use Client.fromBrowser, Client.fromSession, Client.fromDevKey, or Client.fromImpersonation instead.") open func setEndpoint(_ endPoint: String) -> Client { - if !endPoint.hasPrefix("http://") && !endPoint.hasPrefix("https://") { - fatalError("Invalid endpoint URL: \(endPoint)") - } - - self.endPoint = endPoint - self.endPointRealtime = endPoint - .replacingOccurrences(of: "http://", with: "ws://") - .replacingOccurrences(of: "https://", with: "wss://") - + configureEndpoint(endPoint) return self } @@ -191,12 +405,9 @@ open class Client { /// /// @return Client /// + @available(*, deprecated, message: "Use Client.fromBrowser, Client.fromSession, Client.fromDevKey, or Client.fromImpersonation instead.") open func setEndpointRealtime(_ endPoint: String) -> Client { - if !endPoint.hasPrefix("ws://") && !endPoint.hasPrefix("wss://") { - fatalError("Invalid realtime endpoint URL: \(endPoint)") - } - - self.endPointRealtime = endPoint + configureEndpointRealtime(endPoint) return self } @@ -221,6 +432,10 @@ open class Client { return self.headers } + public func getConfig(key: String) -> String? { + return config[key] + } + /// /// Builds a query string from parameters /// @@ -406,7 +621,7 @@ open class Client { } } - func chunkedUpload( + public func chunkedUpload( path: String, headers: inout [String: String], params: inout [String: Any?], diff --git a/templates/apple/Sources/Service.swift.twig b/templates/apple/Sources/Service.swift.twig new file mode 100644 index 0000000000..681c35ef4c --- /dev/null +++ b/templates/apple/Sources/Service.swift.twig @@ -0,0 +1,6 @@ +open class Service { + + public init(_ client: ClientAuth) + { + } +} diff --git a/templates/apple/Sources/Services/Realtime.swift.twig b/templates/apple/Sources/Services/Realtime.swift.twig index e0bef860e9..9f1636d44f 100644 --- a/templates/apple/Sources/Services/Realtime.swift.twig +++ b/templates/apple/Sources/Services/Realtime.swift.twig @@ -5,6 +5,8 @@ import NIOHTTP1 open class Realtime : Service { + private let client: ClientAuth + // Diagnostic messages go to stderr so they don't interleave with any stdout // the host process may be asserting on (e.g. SDK integration tests). private static func logDiagnostic(_ message: String) { @@ -33,6 +35,12 @@ open class Realtime : Service { private var onCloseCallbacks: [(() -> Void)] = [] private var onOpenCallbacks: [(() -> Void)] = [] + public override init(_ client: ClientAuth) + { + self.client = client + super.init(client) + } + public func onError(_ callback: @escaping (Swift.Error?, HTTPResponseStatus?) -> Void) { self.onErrorCallbacks.append(callback) } @@ -75,7 +83,7 @@ open class Realtime : Service { return } - let queryParams = "project=\(client.config["project"]!)" + let queryParams = "project=\(client.getConfig(key: "project")!)" let url = "\(client.endPointRealtime!)/realtime?\(queryParams)" diff --git a/templates/apple/Sources/Services/Service.swift.twig b/templates/apple/Sources/Services/Service.swift.twig index 65c2519399..9c1436bd75 100644 --- a/templates/apple/Sources/Services/Service.swift.twig +++ b/templates/apple/Sources/Services/Service.swift.twig @@ -8,6 +8,14 @@ import {{spec.title | caseUcfirst}}Models /// {{ service.description }} open class {{ service.name | caseUcfirst | overrideIdentifier }}: Service { + private let _client: ClientAuth + + public override init(_ client: ClientAuth) + { + self._client = client + super.init(client) + } + {%~ for method in service.methods %} /// {%~ if method.description %} @@ -46,7 +54,8 @@ open class {{ service.name | caseUcfirst | overrideIdentifier }}: Service { onProgress: ((UploadProgress) -> Void)? = nil {%~ endif %} ) async throws -> {{ method | returnType(spec) | raw }} { - {{~ include('swift/base/params.twig') }} + let client = _client + {{~ include('apple/base/params.twig') }} {%~ if method.type == 'webAuth' %} {{~ include('apple/base/requests/oauth.twig') }} {%~ elseif method.type == 'location' %} diff --git a/templates/apple/base/params.twig b/templates/apple/base/params.twig new file mode 100644 index 0000000000..5ca153e560 --- /dev/null +++ b/templates/apple/base/params.twig @@ -0,0 +1,27 @@ + let apiPath: String = "{{ method.path }}" + {%~ for parameter in method.parameters.path %} + .replacingOccurrences(of: "{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}", with: {{ parameter.name | caseCamel | escapeSwiftKeyword }}{% if parameter.enumValues is not empty %}.rawValue{% endif %}) + {%~ endfor %} + + {%~ if method.parameters.query | merge(method.parameters.body) | length <= 0 %} + let apiParams: [String: Any] = [:] + {%~ else %} + {% if 'multipart/form-data' in method.consumes -%} var + {%- else -%} let + {%- endif %} apiParams: [String: Any?] = [ + {%~ for parameter in method.parameters.query | merge(method.parameters.body) %} + "{{ parameter.name }}": {{ parameter.name | caseCamel | escapeSwiftKeyword }}{% if not loop.last or (method.type == 'location' or method.type == 'webAuth' and method.auth | length > 0) %},{% endif %} + + {%~ endfor %} + {%~ if method.type == 'location' or method.type == 'webAuth' %} + {%~ if method.auth | length > 0 %} + {%~ for node in method.auth %} + {%~ for key,header in node | keys %} + "{{ header | caseLower }}": client.getConfig(key: "{{ header | caseLower }}"){% if not loop.last %},{% endif %} + + {%~ endfor %} + {%~ endfor %} + {%~ endif %} + {%~ endif %} + ] + {%~ endif %} diff --git a/templates/apple/base/requests/oauth.twig b/templates/apple/base/requests/oauth.twig index a91f224147..a9983ef14a 100644 --- a/templates/apple/base/requests/oauth.twig +++ b/templates/apple/base/requests/oauth.twig @@ -1,6 +1,6 @@ let query = "?\(client.parametersToQueryString(params: apiParams))" let url = URL(string: client.endPoint + apiPath + query)! - let callbackScheme = "appwrite-callback-\(client.config["project"] ?? "")" + let callbackScheme = "appwrite-callback-\(client.getConfig(key: "project") ?? "")" _ = try await withCheckedThrowingContinuation { continuation in /// main thread for PresentationContextProvider From f81b78a6eb72498f6b97ebc40aeb009583f961d8 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 6 May 2026 17:23:35 +0530 Subject: [PATCH 37/69] Address Apple factory review comments --- templates/apple/Sources/Client.swift.twig | 69 +++++++++++++++-------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index 51a03fc6d7..7be9281203 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -128,7 +128,8 @@ open class Client: ClientAuth { projectId: String, endpointRealtime: String? = nil, locale: String? = nil, - selfSigned: Bool = false + selfSigned: Bool = false, + compression: Bool = true ) -> ClientAuth { let client = Client() configure( @@ -137,7 +138,8 @@ open class Client: ClientAuth { endpointRealtime: endpointRealtime, projectId: projectId, locale: locale, - selfSigned: selfSigned + selfSigned: selfSigned, + compression: compression ) return client } @@ -148,7 +150,8 @@ open class Client: ClientAuth { session: String, endpointRealtime: String? = nil, locale: String? = nil, - selfSigned: Bool = false + selfSigned: Bool = false, + compression: Bool = true ) -> ClientAuth { let client = Client() configure( @@ -157,7 +160,8 @@ open class Client: ClientAuth { endpointRealtime: endpointRealtime, projectId: projectId, locale: locale, - selfSigned: selfSigned + selfSigned: selfSigned, + compression: compression ) setHeader(client, configKey: "session", header: "X-Appwrite-Session", value: session) return client @@ -169,7 +173,8 @@ open class Client: ClientAuth { devKey: String, endpointRealtime: String? = nil, locale: String? = nil, - selfSigned: Bool = false + selfSigned: Bool = false, + compression: Bool = true ) -> ClientAuth { let client = Client() configure( @@ -178,7 +183,8 @@ open class Client: ClientAuth { endpointRealtime: endpointRealtime, projectId: projectId, locale: locale, - selfSigned: selfSigned + selfSigned: selfSigned, + compression: compression ) setHeader(client, configKey: "devkey", header: "X-Appwrite-Dev-Key", value: devKey) return client @@ -193,10 +199,13 @@ open class Client: ClientAuth { userPhone: String? = nil, endpointRealtime: String? = nil, locale: String? = nil, - selfSigned: Bool = false - ) -> ClientAuth { + selfSigned: Bool = false, + compression: Bool = true + ) throws -> ClientAuth { let targetCount = [userId, userEmail, userPhone].compactMap { $0 }.count - precondition(targetCount == 1, "Provide exactly one impersonation target.") + guard targetCount == 1 else { + throw {{ spec.title | caseUcfirst }}Error(message: "Provide exactly one impersonation target.") + } let client = Client() configure( @@ -205,7 +214,8 @@ open class Client: ClientAuth { endpointRealtime: endpointRealtime, projectId: projectId, locale: locale, - selfSigned: selfSigned + selfSigned: selfSigned, + compression: compression ) setHeader(client, configKey: "session", header: "X-Appwrite-Session", value: session) @@ -230,7 +240,8 @@ open class Client: ClientAuth { endpointRealtime: String?, projectId: String, locale: String?, - selfSigned: Bool + selfSigned: Bool, + compression: Bool ) { client.configureEndpoint(endpoint) @@ -242,6 +253,10 @@ open class Client: ClientAuth { client.configureSelfSigned(selfSigned) } + if !compression { + client.configureCompression(compression) + } + setHeader(client, configKey: "project", header: "X-Appwrite-Project", value: projectId) if let locale = locale { @@ -260,6 +275,23 @@ open class Client: ClientAuth { http = Client.createHTTP(selfSigned: status, compression: compression) } + private func configureCompression(_ status: Bool) { + self.compression = status + + if status { + if compressionHeaderInjected && self.headers["accept-encoding"] == "identity" { + self.headers.removeValue(forKey: "accept-encoding") + } + self.compressionHeaderInjected = false + } else { + self.headers["accept-encoding"] = "identity" + self.compressionHeaderInjected = true + } + + try! http.syncShutdown() + http = Client.createHTTP(selfSigned: selfSigned, compression: status) + } + private func configureEndpoint(_ endPoint: String) { if !endPoint.hasPrefix("http://") && !endPoint.hasPrefix("https://") { fatalError("Invalid endpoint URL: \(endPoint)") @@ -368,20 +400,7 @@ open class Client: ClientAuth { /// @return Client The same client instance. /// open func setCompression(_ status: Bool = true) -> Client { - self.compression = status - - if status { - if compressionHeaderInjected && self.headers["accept-encoding"] == "identity" { - self.headers.removeValue(forKey: "accept-encoding") - } - self.compressionHeaderInjected = false - } else { - self.headers["accept-encoding"] = "identity" - self.compressionHeaderInjected = true - } - - try! http.syncShutdown() - http = Client.createHTTP(selfSigned: selfSigned, compression: status) + configureCompression(status) return self } From 8523d2f3673d9ee612ac48f346acad2942108f1a Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 11:04:02 +0530 Subject: [PATCH 38/69] Add Android client auth factories --- .../src/main/java/io/package/Client.kt.twig | 271 ++++++++++++++++-- .../src/main/java/io/package/Service.kt.twig | 4 +- .../java/io/package/services/Realtime.kt.twig | 14 +- .../java/io/package/services/Service.kt.twig | 10 +- 4 files changed, 265 insertions(+), 34 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Client.kt.twig b/templates/android/library/src/main/java/io/package/Client.kt.twig index 8eb0016a23..a4e9219a88 100644 --- a/templates/android/library/src/main/java/io/package/Client.kt.twig +++ b/templates/android/library/src/main/java/io/package/Client.kt.twig @@ -37,12 +37,47 @@ import javax.net.ssl.X509TrustManager import kotlin.coroutines.CoroutineContext import kotlin.coroutines.resume +interface ClientAuth { + val endpoint: String + val endpointRealtime: String? + + fun getConfig(key: String): String? + + fun getHeaders(): Map + + fun getCookies(url: String): List + + fun getHttpClient(): OkHttpClient + + suspend fun ping(): String + + suspend fun call( + method: String, + path: String, + headers: Map = mapOf(), + params: Map = mapOf(), + responseType: Class, + converter: ((Any) -> T)? = null + ): T + + suspend fun chunkedUpload( + path: String, + headers: MutableMap, + params: MutableMap, + responseType: Class, + converter: ((Any) -> T), + paramName: String, + idParamName: String? = null, + onProgress: ((UploadProgress) -> Unit)? = null, + ): T +} + class Client @JvmOverloads constructor( context: Context, - var endpoint: String = "{{spec.endpoint}}", - var endpointRealtime: String? = null, + override var endpoint: String = "{{spec.endpoint}}", + override var endpointRealtime: String? = null, private var selfSigned: Boolean = false -) : CoroutineScope { +) : ClientAuth, CoroutineScope { companion object { /** @@ -51,6 +86,184 @@ class Client @JvmOverloads constructor( internal const val CHUNK_SIZE = 5*1024*1024; // 5MB internal const val GLOBAL_PREFS = "{{ sdk.namespace | caseDot }}" internal const val COOKIE_PREFS = "myCookie" + + @JvmStatic + @JvmOverloads + fun fromBrowser( + context: Context, + projectId: String, + endpoint: String = "{{spec.endpoint}}", + endpointRealtime: String? = null, + locale: String? = null, + selfSigned: Boolean = false + ): ClientAuth = fromBrowserClient( + context = context, + endpoint = endpoint, + projectId = projectId, + endpointRealtime = endpointRealtime, + locale = locale, + selfSigned = selfSigned + ) + + private fun fromBrowserClient( + context: Context, + endpoint: String, + projectId: String, + endpointRealtime: String?, + locale: String?, + selfSigned: Boolean + ): Client { + val client = Client( + context, + endpoint, + endpointRealtime ?: endpoint.replaceFirst("http", "ws"), + selfSigned + ) + + setHeader( + client, + "project", + "X-{{ spec.title | caseUcfirst }}-Project", + projectId + ) + if (locale != null) { + setHeader( + client, + "locale", + "X-{{ spec.title | caseUcfirst }}-Locale", + locale + ) + } + + return client + } + + private fun setHeader( + client: Client, + configKey: String, + header: String, + value: String + ) { + client.config[configKey] = value + client.addHeader(header, value) + } + + @JvmStatic + @JvmOverloads + fun fromSession( + context: Context, + projectId: String, + session: String, + endpoint: String = "{{spec.endpoint}}", + endpointRealtime: String? = null, + locale: String? = null, + selfSigned: Boolean = false + ): ClientAuth { + val client = fromBrowserClient( + context = context, + endpoint = endpoint, + projectId = projectId, + endpointRealtime = endpointRealtime, + locale = locale, + selfSigned = selfSigned + ) + + setHeader( + client, + "session", + "X-{{ spec.title | caseUcfirst }}-Session", + session + ) + return client + } + + @JvmStatic + @JvmOverloads + fun fromDevKey( + context: Context, + projectId: String, + devKey: String, + endpoint: String = "{{spec.endpoint}}", + endpointRealtime: String? = null, + locale: String? = null, + selfSigned: Boolean = false + ): ClientAuth { + val client = fromBrowserClient( + context = context, + endpoint = endpoint, + projectId = projectId, + endpointRealtime = endpointRealtime, + locale = locale, + selfSigned = selfSigned + ) + + setHeader( + client, + "devKey", + "X-{{ spec.title | caseUcfirst }}-Dev-Key", + devKey + ) + return client + } + + @JvmStatic + @JvmOverloads + fun fromImpersonation( + context: Context, + projectId: String, + session: String, + userId: String? = null, + userEmail: String? = null, + userPhone: String? = null, + endpoint: String = "{{spec.endpoint}}", + endpointRealtime: String? = null, + locale: String? = null, + selfSigned: Boolean = false + ): ClientAuth { + val targets = listOfNotNull(userId, userEmail, userPhone).size + require(targets == 1) { + "Exactly one of userId, userEmail, or userPhone must be provided." + } + + val client = fromBrowserClient( + context = context, + endpoint = endpoint, + projectId = projectId, + endpointRealtime = endpointRealtime, + locale = locale, + selfSigned = selfSigned + ) + + setHeader( + client, + "session", + "X-{{ spec.title | caseUcfirst }}-Session", + session + ) + + when { + userId != null -> setHeader( + client, + "impersonateUserId", + "X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id", + userId + ) + userEmail != null -> setHeader( + client, + "impersonateUserEmail", + "X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email", + userEmail + ) + userPhone != null -> setHeader( + client, + "impersonateUserPhone", + "X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone", + userPhone + ) + } + + return client + } } override val coroutineContext: CoroutineContext @@ -96,7 +309,7 @@ class Client @JvmOverloads constructor( ) config = mutableMapOf() - setSelfSigned(selfSigned) + applySelfSigned(selfSigned) } {% for header in spec.global.headers %} @@ -111,6 +324,7 @@ class Client @JvmOverloads constructor( * * @return this */ + @Deprecated("Use Client.fromBrowser or another factory method instead.") fun set{{header.key | caseUcfirst}}(value: String): Client { config["{{ header.key | caseCamel }}"] = value addHeader("{{ header.name | caseLower }}", value) @@ -125,7 +339,13 @@ class Client @JvmOverloads constructor( * * @return this */ + @Deprecated("Use Client.fromBrowser or another factory method instead.") fun setSelfSigned(status: Boolean): Client { + applySelfSigned(status) + return this + } + + private fun applySelfSigned(status: Boolean) { selfSigned = status val builder = OkHttpClient() @@ -134,7 +354,7 @@ class Client @JvmOverloads constructor( if (!selfSigned) { http = builder.build() - return this + return } try { @@ -166,8 +386,6 @@ class Client @JvmOverloads constructor( } catch (e: Exception) { throw RuntimeException(e) } - - return this } /** @@ -178,6 +396,7 @@ class Client @JvmOverloads constructor( * @return this */ @Throws(IllegalArgumentException::class) + @Deprecated("Use Client.fromBrowser or another factory method instead.") fun setEndpoint(endpoint: String): Client { require(endpoint.startsWith("http://") || endpoint.startsWith("https://")) { "Invalid endpoint URL: $endpoint" @@ -197,6 +416,7 @@ class Client @JvmOverloads constructor( * @return this */ @Throws(IllegalArgumentException::class) + @Deprecated("Use Client.fromBrowser or another factory method instead.") fun setEndpointRealtime(endpoint: String): Client { require(endpoint.startsWith("ws://") || endpoint.startsWith("wss://")) { "Invalid realtime endpoint URL: $endpoint" @@ -224,7 +444,15 @@ class Client @JvmOverloads constructor( * * @return a copy of the current request headers */ - fun getHeaders(): Map = headers.toMap() + override fun getHeaders(): Map = headers.toMap() + + /** + * Get a configuration value by key. + * + * @param key the configuration key + * @return the configuration value if set + */ + override fun getConfig(key: String): String? = config[key] /** * Get the cookies for a given URL from the SDK's cookie store. @@ -232,21 +460,21 @@ class Client @JvmOverloads constructor( * @param url the URL to retrieve cookies for * @return a list of cookies for the given URL */ - fun getCookies(url: String): List = cookieJar.loadForRequest(url.toHttpUrl()) + override fun getCookies(url: String): List = cookieJar.loadForRequest(url.toHttpUrl()) /** * Get the OkHttpClient instance used by this SDK. * * @return the OkHttpClient instance used by this client */ - fun getHttpClient(): OkHttpClient = http + override fun getHttpClient(): OkHttpClient = http /** * Sends a "ping" request to Appwrite to verify connectivity. * * @return String */ - suspend fun ping(): String { + override suspend fun ping(): String { val apiPath = "/ping" val apiParams = mutableMapOf() val apiHeaders = mutableMapOf("content-type" to "application/json") @@ -256,7 +484,8 @@ class Client @JvmOverloads constructor( apiPath, apiHeaders, apiParams, - responseType = String::class.java + responseType = String::class.java, + converter = null ) } @@ -271,13 +500,13 @@ class Client @JvmOverloads constructor( * @return [T] */ @Throws({{ spec.title | caseUcfirst }}Exception::class) - suspend fun call( + override suspend fun call( method: String, path: String, - headers: Map = mapOf(), - params: Map = mapOf(), + headers: Map, + params: Map, responseType: Class, - converter: ((Any) -> T)? = null + converter: ((Any) -> T)? ): T { val filteredParams = params.filterValues { it != null } @@ -364,15 +593,15 @@ class Client @JvmOverloads constructor( * @return [T] */ @Throws({{ spec.title | caseUcfirst }}Exception::class) - suspend fun chunkedUpload( + override suspend fun chunkedUpload( path: String, headers: MutableMap, params: MutableMap, responseType: Class, converter: ((Any) -> T), paramName: String, - idParamName: String? = null, - onProgress: ((UploadProgress) -> Unit)? = null, + idParamName: String?, + onProgress: ((UploadProgress) -> Unit)?, ): T { var file: RandomAccessFile? = null val input = params[paramName] as InputFile @@ -420,6 +649,7 @@ class Client @JvmOverloads constructor( headers = headers, params = emptyMap(), responseType = Map::class.java, + converter = null ) val chunksUploaded = current["chunksUploaded"] as Long offset = chunksUploaded * CHUNK_SIZE @@ -460,7 +690,8 @@ class Client @JvmOverloads constructor( path, headers, params, - responseType = Map::class.java + responseType = Map::class.java, + converter = null ) offset += CHUNK_SIZE diff --git a/templates/android/library/src/main/java/io/package/Service.kt.twig b/templates/android/library/src/main/java/io/package/Service.kt.twig index 332fc958ab..8f0f5f6ca3 100644 --- a/templates/android/library/src/main/java/io/package/Service.kt.twig +++ b/templates/android/library/src/main/java/io/package/Service.kt.twig @@ -1,10 +1,10 @@ package {{ sdk.namespace | caseDot }} -import {{ sdk.namespace | caseDot }}.Client +import {{ sdk.namespace | caseDot }}.ClientAuth /** * Abstract class for services. * * @param client The Appwrite client. */ -abstract class Service(val client: Client) +abstract class Service(@Suppress("UNUSED_PARAMETER") client: ClientAuth) diff --git a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig index 888d0fa6e2..98b31d59a6 100644 --- a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig @@ -1,7 +1,7 @@ package {{ sdk.namespace | caseDot }}.services import {{ sdk.namespace | caseDot }}.Service -import {{ sdk.namespace | caseDot }}.Client +import {{ sdk.namespace | caseDot }}.ClientAuth import {{ sdk.namespace | caseDot }}.Channel import {{ sdk.namespace | caseDot }}.ID import {{ sdk.namespace | caseDot }}.Query @@ -25,7 +25,7 @@ import java.util.concurrent.atomic.AtomicInteger import android.util.Log import kotlin.coroutines.CoroutineContext -class Realtime(client: Client) : Service(client), CoroutineScope { +class Realtime(private val client: ClientAuth) : Service(client), CoroutineScope { private val job = Job() @@ -64,7 +64,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { return } - val encodedProject = java.net.URLEncoder.encode(client.config["project"].toString(), "UTF-8") + val encodedProject = java.net.URLEncoder.encode(client.getConfig("project").toString(), "UTF-8") val queryParams = "project=$encodedProject" val url = "${client.endpointRealtime}/realtime?$queryParams" request = Request.Builder().url(url).build() @@ -82,14 +82,14 @@ class Realtime(client: Client) : Service(client), CoroutineScope { originalRequest = request, listener = {{ spec.title | caseUcfirst }}WebSocketListener(generation), random = Random(), - pingIntervalMillis = client.http.pingIntervalMillis.toLong(), + pingIntervalMillis = client.getHttpClient().pingIntervalMillis.toLong(), extensions = null, - minimumDeflateSize = client.http.minWebSocketMessageToCompress + minimumDeflateSize = client.getHttpClient().minWebSocketMessageToCompress ) socket = newSocket } - newSocket?.connect(client.http) + newSocket?.connect(client.getHttpClient()) } private fun closeSocket() { @@ -422,4 +422,4 @@ class Realtime(client: Client) : Service(client), CoroutineScope { t.printStackTrace() } } -} \ No newline at end of file +} diff --git a/templates/android/library/src/main/java/io/package/services/Service.kt.twig b/templates/android/library/src/main/java/io/package/services/Service.kt.twig index 6ac78af9d2..0acfade6bd 100644 --- a/templates/android/library/src/main/java/io/package/services/Service.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/Service.kt.twig @@ -1,7 +1,7 @@ package {{ sdk.namespace | caseDot }}.services import android.net.Uri -import {{ sdk.namespace | caseDot }}.Client +import {{ sdk.namespace | caseDot }}.ClientAuth import {{ sdk.namespace | caseDot }}.Service {% if spec.definitions is not empty %} import {{ sdk.namespace | caseDot }}.models.* @@ -25,7 +25,7 @@ import java.io.File /** * {{ service.description | replace({"\n": "\n * "}) | raw }} */ -class {{ service.name | caseUcfirst }}(client: Client) : Service(client) { +class {{ service.name | caseUcfirst }}(private val client: ClientAuth) : Service(client) { {% for method in service.methods %} /** @@ -80,7 +80,7 @@ class {{ service.name | caseUcfirst }}(client: Client) : Service(client) { {%~ if method.auth | length > 0 %} {%~ for node in method.auth %} {%~ for key,header in node | keys %} - "{{ header | caseLower }}" to client.config["{{ header | caseLower }}"], + "{{ header | caseLower }}" to client.getConfig("{{ header | caseLower }}"), {%~ endfor %} {%~ endfor %} {%~ endif %} @@ -105,7 +105,7 @@ class {{ service.name | caseUcfirst }}(client: Client) : Service(client) { } val apiUrl = Uri.parse("${client.endpoint}${apiPath}?${apiQuery.joinToString("&")}") - val callbackUrlScheme = "{{ spec.title | caseLower }}-callback-${client.config["project"]}" + val callbackUrlScheme = "{{ spec.title | caseLower }}-callback-${client.getConfig("project")}" WebAuthComponent.authenticate(activity, apiUrl, callbackUrlScheme) { if (it.isFailure) { @@ -126,7 +126,7 @@ class {{ service.name | caseUcfirst }}(client: Client) : Service(client) { .httpOnly() .build() - client.http.cookieJar.saveFromResponse( + client.getHttpClient().cookieJar.saveFromResponse( client.endpoint.toHttpUrl(), listOf(cookie) ) From 037a4133dd3ef4ae567ed63cc32040ddb69515d0 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 11:28:41 +0530 Subject: [PATCH 39/69] Fix Greptile isomorphic SDK review comments --- .../src/main/java/io/package/Client.kt.twig | 90 +++++++++++++++++-- .../java/io/package/services/Service.kt.twig | 2 + templates/apple/Sources/Client.swift.twig | 60 +++++++------ templates/web/src/services/template.ts.twig | 9 +- 4 files changed, 120 insertions(+), 41 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Client.kt.twig b/templates/android/library/src/main/java/io/package/Client.kt.twig index a4e9219a88..d372facd14 100644 --- a/templates/android/library/src/main/java/io/package/Client.kt.twig +++ b/templates/android/library/src/main/java/io/package/Client.kt.twig @@ -51,27 +51,63 @@ interface ClientAuth { suspend fun ping(): String - suspend fun call( + suspend fun callInternal( method: String, path: String, - headers: Map = mapOf(), - params: Map = mapOf(), + headers: Map, + params: Map, responseType: Class, - converter: ((Any) -> T)? = null + converter: ((Any) -> T)? ): T - suspend fun chunkedUpload( + suspend fun chunkedUploadInternal( path: String, headers: MutableMap, params: MutableMap, responseType: Class, converter: ((Any) -> T), paramName: String, - idParamName: String? = null, - onProgress: ((UploadProgress) -> Unit)? = null, + idParamName: String?, + onProgress: ((UploadProgress) -> Unit)?, ): T } +suspend fun ClientAuth.call( + method: String, + path: String = "", + headers: Map = mapOf(), + params: Map = mapOf(), + responseType: Class, + converter: ((Any) -> T)? = null +): T = callInternal( + method = method, + path = path, + headers = headers, + params = params, + responseType = responseType, + converter = converter +) + +suspend fun ClientAuth.chunkedUpload( + path: String, + headers: MutableMap, + params: MutableMap, + responseType: Class, + converter: ((Any) -> T), + paramName: String, + idParamName: String? = null, + onProgress: ((UploadProgress) -> Unit)? = null, +): T = chunkedUploadInternal( + path = path, + headers = headers, + params = params, + responseType = responseType, + converter = converter, + paramName = paramName, + idParamName = idParamName, + onProgress = onProgress +) + class Client @JvmOverloads constructor( context: Context, override var endpoint: String = "{{spec.endpoint}}", @@ -500,7 +536,23 @@ class Client @JvmOverloads constructor( * @return [T] */ @Throws({{ spec.title | caseUcfirst }}Exception::class) - override suspend fun call( + suspend fun call( + method: String, + path: String = "", + headers: Map = mapOf(), + params: Map = mapOf(), + responseType: Class, + converter: ((Any) -> T)? = null + ): T = callInternal( + method = method, + path = path, + headers = headers, + params = params, + responseType = responseType, + converter = converter + ) + + override suspend fun callInternal( method: String, path: String, headers: Map, @@ -593,7 +645,27 @@ class Client @JvmOverloads constructor( * @return [T] */ @Throws({{ spec.title | caseUcfirst }}Exception::class) - override suspend fun chunkedUpload( + suspend fun chunkedUpload( + path: String, + headers: MutableMap, + params: MutableMap, + responseType: Class, + converter: ((Any) -> T), + paramName: String, + idParamName: String? = null, + onProgress: ((UploadProgress) -> Unit)? = null, + ): T = chunkedUploadInternal( + path = path, + headers = headers, + params = params, + responseType = responseType, + converter = converter, + paramName = paramName, + idParamName = idParamName, + onProgress = onProgress + ) + + override suspend fun chunkedUploadInternal( path: String, headers: MutableMap, params: MutableMap, diff --git a/templates/android/library/src/main/java/io/package/services/Service.kt.twig b/templates/android/library/src/main/java/io/package/services/Service.kt.twig index 0acfade6bd..3401a0a851 100644 --- a/templates/android/library/src/main/java/io/package/services/Service.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/Service.kt.twig @@ -3,6 +3,8 @@ package {{ sdk.namespace | caseDot }}.services import android.net.Uri import {{ sdk.namespace | caseDot }}.ClientAuth import {{ sdk.namespace | caseDot }}.Service +import {{ sdk.namespace | caseDot }}.call +import {{ sdk.namespace | caseDot }}.chunkedUpload {% if spec.definitions is not empty %} import {{ sdk.namespace | caseDot }}.models.* {% endif %} diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index 7be9281203..66ac411f9a 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -130,9 +130,9 @@ open class Client: ClientAuth { locale: String? = nil, selfSigned: Bool = false, compression: Bool = true - ) -> ClientAuth { + ) throws -> ClientAuth { let client = Client() - configure( + try configure( client, endpoint: endpoint, endpointRealtime: endpointRealtime, @@ -152,9 +152,9 @@ open class Client: ClientAuth { locale: String? = nil, selfSigned: Bool = false, compression: Bool = true - ) -> ClientAuth { + ) throws -> ClientAuth { let client = Client() - configure( + try configure( client, endpoint: endpoint, endpointRealtime: endpointRealtime, @@ -175,9 +175,9 @@ open class Client: ClientAuth { locale: String? = nil, selfSigned: Bool = false, compression: Bool = true - ) -> ClientAuth { + ) throws -> ClientAuth { let client = Client() - configure( + try configure( client, endpoint: endpoint, endpointRealtime: endpointRealtime, @@ -208,7 +208,7 @@ open class Client: ClientAuth { } let client = Client() - configure( + try configure( client, endpoint: endpoint, endpointRealtime: endpointRealtime, @@ -242,20 +242,14 @@ open class Client: ClientAuth { locale: String?, selfSigned: Bool, compression: Bool - ) { - client.configureEndpoint(endpoint) + ) throws { + try client.configureEndpoint(endpoint) if let endpointRealtime = endpointRealtime { - client.configureEndpointRealtime(endpointRealtime) - } - - if selfSigned { - client.configureSelfSigned(selfSigned) + try client.configureEndpointRealtime(endpointRealtime) } - if !compression { - client.configureCompression(compression) - } + client.configureConnection(selfSigned: selfSigned, compression: compression) setHeader(client, configKey: "project", header: "X-Appwrite-Project", value: projectId) @@ -270,15 +264,19 @@ open class Client: ClientAuth { } private func configureSelfSigned(_ status: Bool) { - self.selfSigned = status - try! http.syncShutdown() - http = Client.createHTTP(selfSigned: status, compression: compression) + configureConnection(selfSigned: status, compression: compression) } private func configureCompression(_ status: Bool) { - self.compression = status + configureConnection(selfSigned: selfSigned, compression: status) + } - if status { + private func configureConnection(selfSigned: Bool, compression: Bool) { + let shouldRecreateHTTP = self.selfSigned != selfSigned || self.compression != compression + self.selfSigned = selfSigned + self.compression = compression + + if compression { if compressionHeaderInjected && self.headers["accept-encoding"] == "identity" { self.headers.removeValue(forKey: "accept-encoding") } @@ -288,13 +286,17 @@ open class Client: ClientAuth { self.compressionHeaderInjected = true } + guard shouldRecreateHTTP else { + return + } + try! http.syncShutdown() - http = Client.createHTTP(selfSigned: selfSigned, compression: status) + http = Client.createHTTP(selfSigned: selfSigned, compression: compression) } - private func configureEndpoint(_ endPoint: String) { + private func configureEndpoint(_ endPoint: String) throws { if !endPoint.hasPrefix("http://") && !endPoint.hasPrefix("https://") { - fatalError("Invalid endpoint URL: \(endPoint)") + throw {{ spec.title | caseUcfirst }}Error(message: "Invalid endpoint URL: \(endPoint)") } self.endPoint = endPoint @@ -303,9 +305,9 @@ open class Client: ClientAuth { .replacingOccurrences(of: "https://", with: "wss://") } - private func configureEndpointRealtime(_ endPoint: String) { + private func configureEndpointRealtime(_ endPoint: String) throws { if !endPoint.hasPrefix("ws://") && !endPoint.hasPrefix("wss://") { - fatalError("Invalid realtime endpoint URL: \(endPoint)") + throw {{ spec.title | caseUcfirst }}Error(message: "Invalid realtime endpoint URL: \(endPoint)") } self.endPointRealtime = endPoint @@ -413,7 +415,7 @@ open class Client: ClientAuth { /// @available(*, deprecated, message: "Use Client.fromBrowser, Client.fromSession, Client.fromDevKey, or Client.fromImpersonation instead.") open func setEndpoint(_ endPoint: String) -> Client { - configureEndpoint(endPoint) + try! configureEndpoint(endPoint) return self } @@ -426,7 +428,7 @@ open class Client: ClientAuth { /// @available(*, deprecated, message: "Use Client.fromBrowser, Client.fromSession, Client.fromDevKey, or Client.fromImpersonation instead.") open func setEndpointRealtime(_ endPoint: String) -> Client { - configureEndpointRealtime(endPoint) + try! configureEndpointRealtime(endPoint) return self } diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index 51b8f8c80c..01059957d5 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -54,7 +54,7 @@ class {{ service.name | caseUcfirst }}Runtime { {%~ endif %} {%~ for method in service.methods %} - {%~ set thisGate = '' %} + {%~ set thisGate = method | webMethodThisGate(service) %} /** {%~ if method.description %} * {{ method.description | replace({'\n': '\n * '}) | raw }} @@ -89,7 +89,7 @@ class {{ service.name | caseUcfirst }}Runtime { */ {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({{ thisGate | raw }}{% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress?: (progress: UploadProgress) => void{% endif %}): {{ method | getReturn(spec) | raw }}; {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}( - {{ thisGate | raw }}{% if method.parameters.all|length > 0 %}paramsOrFirst{% if not method.parameters.all[0].required or method.parameters.all[0].nullable %}?{% endif %}: { {% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress?: (progress: UploadProgress) => void{% endif %} } | {{ method.parameters.all[0] | getPropertyType(method) | raw }}{% if method.parameters.all|length > 1 %}, + {% if method.parameters.all|length > 0 %}paramsOrFirst{% if not method.parameters.all[0].required or method.parameters.all[0].nullable %}?{% endif %}: { {% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress?: (progress: UploadProgress) => void{% endif %} } | {{ method.parameters.all[0] | getPropertyType(method) | raw }}{% if method.parameters.all|length > 1 %}, ...rest: [{% for parameter in method.parameters.all[1:] %}({{ parameter | getPropertyType(method) | raw }})?{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %},((progress: UploadProgress) => void)?{% endif %}]{% endif %}{% endif %} ): {{ method | getReturn(spec) | raw }} { @@ -129,7 +129,10 @@ class {{ service.name | caseUcfirst }}Runtime { {%~ endif %} {%~ endif %} {%~ else %} - {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({{ thisGate | raw }}{% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress = (progress: UploadProgress) => void{% endif %}): {{ method | getReturn(spec) | raw }} { + {%~ if thisGate is not empty %} + {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({{ thisGate | raw }}{% if 'multipart/form-data' in method.consumes %}onProgress?: (progress: UploadProgress) => void{% endif %}): {{ method | getReturn(spec) | raw }}; + {%~ endif %} + {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress = (progress: UploadProgress) => void{% endif %}): {{ method | getReturn(spec) | raw }} { {%~ endif %} {%~ for parameter in method.parameters.all %} {%~ if parameter.required %} From a833944fcea62d2ad63ff31777cb934b4800284b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 11:37:14 +0530 Subject: [PATCH 40/69] Use spec-driven Apple auth headers --- templates/apple/Sources/Client.swift.twig | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index 66ac411f9a..59385cb369 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -163,7 +163,7 @@ open class Client: ClientAuth { selfSigned: selfSigned, compression: compression ) - setHeader(client, configKey: "session", header: "X-Appwrite-Session", value: session) + setHeader(client, configKey: "session", header: "X-{{ spec.title | caseUcfirst }}-Session", value: session) return client } @@ -186,7 +186,7 @@ open class Client: ClientAuth { selfSigned: selfSigned, compression: compression ) - setHeader(client, configKey: "devkey", header: "X-Appwrite-Dev-Key", value: devKey) + setHeader(client, configKey: "devkey", header: "X-{{ spec.title | caseUcfirst }}-Dev-Key", value: devKey) return client } @@ -217,18 +217,18 @@ open class Client: ClientAuth { selfSigned: selfSigned, compression: compression ) - setHeader(client, configKey: "session", header: "X-Appwrite-Session", value: session) + setHeader(client, configKey: "session", header: "X-{{ spec.title | caseUcfirst }}-Session", value: session) if let userId = userId { - setHeader(client, configKey: "impersonateuserid", header: "X-Appwrite-Impersonate-User-Id", value: userId) + setHeader(client, configKey: "impersonateuserid", header: "X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id", value: userId) } if let userEmail = userEmail { - setHeader(client, configKey: "impersonateuseremail", header: "X-Appwrite-Impersonate-User-Email", value: userEmail) + setHeader(client, configKey: "impersonateuseremail", header: "X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email", value: userEmail) } if let userPhone = userPhone { - setHeader(client, configKey: "impersonateuserphone", header: "X-Appwrite-Impersonate-User-Phone", value: userPhone) + setHeader(client, configKey: "impersonateuserphone", header: "X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone", value: userPhone) } return client @@ -251,10 +251,10 @@ open class Client: ClientAuth { client.configureConnection(selfSigned: selfSigned, compression: compression) - setHeader(client, configKey: "project", header: "X-Appwrite-Project", value: projectId) + setHeader(client, configKey: "project", header: "X-{{ spec.title | caseUcfirst }}-Project", value: projectId) if let locale = locale { - setHeader(client, configKey: "locale", header: "X-Appwrite-Locale", value: locale) + setHeader(client, configKey: "locale", header: "X-{{ spec.title | caseUcfirst }}-Locale", value: locale) } } From 9ee0b7ba1624b8d2744cda2c643b0967bb2ace9e Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 12:01:46 +0530 Subject: [PATCH 41/69] Add auth factory flow test coverage --- tests/Android14Java11Test.php | 1 + tests/Android14Java17Test.php | 1 + tests/Android14Java8Test.php | 1 + tests/Android5Java17Test.php | 1 + tests/AppleSwift56Test.php | 1 + tests/Base.php | 41 ++++++++++++++++ tests/WebChromiumTest.php | 1 + tests/WebNodeTest.php | 1 + tests/languages/android/Tests.kt | 66 +++++++++++++++++++++++++ tests/languages/apple/Tests.swift | 78 +++++++++++++++++++++++++++++ tests/languages/web/index.html | 81 +++++++++++++++++++++++++++++++ tests/languages/web/node.js | 81 +++++++++++++++++++++++++++++++ 12 files changed, 354 insertions(+) diff --git a/tests/Android14Java11Test.php b/tests/Android14Java11Test.php index 0be9d0edca..bfc2f25b0c 100644 --- a/tests/Android14Java11Test.php +++ b/tests/Android14Java11Test.php @@ -20,6 +20,7 @@ class Android14Java11Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-34-jdk11 sh -c "./gradlew :library:testReleaseUnitTest --stacktrace -q && cat library/result.txt"'; protected array $expectedOutput = [ + ...Base::ANDROID_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/Android14Java17Test.php b/tests/Android14Java17Test.php index 08369666ef..9b71782a5d 100644 --- a/tests/Android14Java17Test.php +++ b/tests/Android14Java17Test.php @@ -20,6 +20,7 @@ class Android14Java17Test extends Base 'docker run --rm --network="mockapi" -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-34-jdk17 sh -c "./gradlew :library:testReleaseUnitTest --stacktrace -q && cat library/result.txt"'; protected array $expectedOutput = [ + ...Base::ANDROID_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/Android14Java8Test.php b/tests/Android14Java8Test.php index 4eea753c2f..c0a85568ed 100644 --- a/tests/Android14Java8Test.php +++ b/tests/Android14Java8Test.php @@ -20,6 +20,7 @@ class Android14Java8Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-34-jdk8 sh -c "./gradlew :library:testReleaseUnitTest --stacktrace -q && cat library/result.txt"'; protected array $expectedOutput = [ + ...Base::ANDROID_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/Android5Java17Test.php b/tests/Android5Java17Test.php index f9199b1477..3525793641 100644 --- a/tests/Android5Java17Test.php +++ b/tests/Android5Java17Test.php @@ -20,6 +20,7 @@ class Android5Java17Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-21-jdk17 sh -c "./gradlew :library:testReleaseUnitTest --stacktrace -q && cat library/result.txt"'; protected array $expectedOutput = [ + ...Base::ANDROID_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/AppleSwift56Test.php b/tests/AppleSwift56Test.php index cc062dba11..a7af09612f 100644 --- a/tests/AppleSwift56Test.php +++ b/tests/AppleSwift56Test.php @@ -19,6 +19,7 @@ class AppleSwift56Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/apple swift:5.6-focal swift test'; protected array $expectedOutput = [ + ...Base::APPLE_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/Base.php b/tests/Base.php index 5da626b5b5..54e695ab22 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -109,6 +109,47 @@ abstract class Base extends TestCase 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', ]; + protected const WEB_AUTH_FACTORY_RESPONSES = [ + 'auth-project', + 'en-US', + 'client', + 'wss://realtime.example.com/v1', + 'auth-session', + 'auth-dev-key', + 'auth-user-id', + 'auth@example.com', + '+15555550123', + 'Exactly one impersonation target must be provided', + 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', + 'Invalid realtime endpoint URL: ftp://cloud.appwrite.io/v1', + ]; + + protected const APPLE_AUTH_FACTORY_RESPONSES = [ + 'auth-project', + 'en-US', + 'wss://realtime.example.com/v1', + 'auth-session', + 'auth-dev-key', + 'auth-user-id', + 'auth@example.com', + '+15555550123', + 'Provide exactly one impersonation target.', + 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', + 'Invalid realtime endpoint URL: ftp://cloud.appwrite.io/v1', + ]; + + protected const ANDROID_AUTH_FACTORY_RESPONSES = [ + 'auth-project', + 'en-US', + 'wss://realtime.example.com/v1', + 'auth-session', + 'auth-dev-key', + 'auth-user-id', + 'auth@example.com', + '+15555550123', + 'Exactly one of userId, userEmail, or userPhone must be provided.', + ]; + protected const REALTIME_RESPONSES = [ 'WS:/v1/realtime:passed', 'WS:/v1/realtime:passed', diff --git a/tests/WebChromiumTest.php b/tests/WebChromiumTest.php index fb62a336b4..9f98ad9d10 100644 --- a/tests/WebChromiumTest.php +++ b/tests/WebChromiumTest.php @@ -21,6 +21,7 @@ class WebChromiumTest extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -e BROWSER=chromium -w /app/tests/sdks/web mcr.microsoft.com/playwright:v1.56.1-jammy node tests.js'; protected array $expectedOutput = [ + ...Base::WEB_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::FOO_RESPONSES, // Object params diff --git a/tests/WebNodeTest.php b/tests/WebNodeTest.php index 4f382219e5..181b6c242a 100644 --- a/tests/WebNodeTest.php +++ b/tests/WebNodeTest.php @@ -22,6 +22,7 @@ class WebNodeTest extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/web node:18-alpine node node.js'; protected array $expectedOutput = [ + ...Base::WEB_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::FOO_RESPONSES, // Object params diff --git a/tests/languages/android/Tests.kt b/tests/languages/android/Tests.kt index d32e0ed6a4..67f16ce1c6 100644 --- a/tests/languages/android/Tests.kt +++ b/tests/languages/android/Tests.kt @@ -73,6 +73,72 @@ class ServiceTest { writeToFile("x-sdk-name: ${sdkHeaders["x-sdk-name"]}; x-sdk-platform: ${sdkHeaders["x-sdk-platform"]}; x-sdk-language: ${sdkHeaders["x-sdk-language"]}; x-sdk-version: ${sdkHeaders["x-sdk-version"]}") + val browserClient = Client.fromBrowser( + context = ApplicationProvider.getApplicationContext(), + projectId = "auth-project", + endpoint = "https://cloud.appwrite.io/v1", + endpointRealtime = "wss://realtime.example.com/v1", + locale = "en-US", + selfSigned = true + ) + writeToFile(browserClient.getConfig("project")) + writeToFile(browserClient.getConfig("locale")) + writeToFile(browserClient.endpointRealtime) + + val sessionClient = Client.fromSession( + context = ApplicationProvider.getApplicationContext(), + projectId = "auth-project", + session = "auth-session", + endpoint = "https://cloud.appwrite.io/v1" + ) + writeToFile(sessionClient.getConfig("session")) + + val devKeyClient = Client.fromDevKey( + context = ApplicationProvider.getApplicationContext(), + projectId = "auth-project", + devKey = "auth-dev-key", + endpoint = "https://cloud.appwrite.io/v1" + ) + writeToFile(devKeyClient.getConfig("devKey")) + + val impersonationUserClient = Client.fromImpersonation( + context = ApplicationProvider.getApplicationContext(), + projectId = "auth-project", + session = "auth-session", + userId = "auth-user-id", + endpoint = "https://cloud.appwrite.io/v1" + ) + writeToFile(impersonationUserClient.getConfig("impersonateUserId")) + + val impersonationEmailClient = Client.fromImpersonation( + context = ApplicationProvider.getApplicationContext(), + projectId = "auth-project", + session = "auth-session", + userEmail = "auth@example.com", + endpoint = "https://cloud.appwrite.io/v1" + ) + writeToFile(impersonationEmailClient.getConfig("impersonateUserEmail")) + + val impersonationPhoneClient = Client.fromImpersonation( + context = ApplicationProvider.getApplicationContext(), + projectId = "auth-project", + session = "auth-session", + userPhone = "+15555550123", + endpoint = "https://cloud.appwrite.io/v1" + ) + writeToFile(impersonationPhoneClient.getConfig("impersonateUserPhone")) + + try { + Client.fromImpersonation( + context = ApplicationProvider.getApplicationContext(), + projectId = "auth-project", + session = "auth-session", + endpoint = "https://cloud.appwrite.io/v1" + ) + } catch (e: IllegalArgumentException) { + writeToFile(e.message) + } + runBlocking { val ping = client.ping() val pingResponse = parse(ping) diff --git a/tests/languages/apple/Tests.swift b/tests/languages/apple/Tests.swift index ed1314b328..2e951a4dd8 100644 --- a/tests/languages/apple/Tests.swift +++ b/tests/languages/apple/Tests.swift @@ -27,6 +27,84 @@ class Tests: XCTestCase { let sdkHeaders = client.getHeaders() print("x-sdk-name: \(sdkHeaders["x-sdk-name"] ?? "nil"); x-sdk-platform: \(sdkHeaders["x-sdk-platform"] ?? "nil"); x-sdk-language: \(sdkHeaders["x-sdk-language"] ?? "nil"); x-sdk-version: \(sdkHeaders["x-sdk-version"] ?? "nil")") + let browserClient = try Client.fromBrowser( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + endpointRealtime: "wss://realtime.example.com/v1", + locale: "en-US", + selfSigned: true + ) + print(browserClient.getConfig(key: "project") ?? "nil") + print(browserClient.getConfig(key: "locale") ?? "nil") + print(browserClient.endPointRealtime ?? "nil") + + let sessionClient = try Client.fromSession( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + session: "auth-session" + ) + print(sessionClient.getConfig(key: "session") ?? "nil") + + let devKeyClient = try Client.fromDevKey( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + devKey: "auth-dev-key" + ) + print(devKeyClient.getConfig(key: "devkey") ?? "nil") + + let impersonationUserClient = try Client.fromImpersonation( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + session: "auth-session", + userId: "auth-user-id" + ) + print(impersonationUserClient.getConfig(key: "impersonateuserid") ?? "nil") + + let impersonationEmailClient = try Client.fromImpersonation( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + session: "auth-session", + userEmail: "auth@example.com" + ) + print(impersonationEmailClient.getConfig(key: "impersonateuseremail") ?? "nil") + + let impersonationPhoneClient = try Client.fromImpersonation( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + session: "auth-session", + userPhone: "+15555550123" + ) + print(impersonationPhoneClient.getConfig(key: "impersonateuserphone") ?? "nil") + + do { + _ = try Client.fromImpersonation( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + session: "auth-session" + ) + } catch { + print(error.localizedDescription) + } + + do { + _ = try Client.fromBrowser( + endpoint: "htp://cloud.appwrite.io/v1", + projectId: "auth-project" + ) + } catch { + print(error.localizedDescription) + } + + do { + _ = try Client.fromBrowser( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + endpointRealtime: "ftp://cloud.appwrite.io/v1" + ) + } catch { + print(error.localizedDescription) + } + // Ping pong test let ping = try await client.ping() let pingResult = parse(from: ping)! diff --git a/tests/languages/web/index.html b/tests/languages/web/index.html index a953a6b6b0..7122bd5496 100644 --- a/tests/languages/web/index.html +++ b/tests/languages/web/index.html @@ -39,6 +39,87 @@ // Ping console.log(`x-sdk-name: ${sdkHeaders['x-sdk-name']}; x-sdk-platform: ${sdkHeaders['x-sdk-platform']}; x-sdk-language: ${sdkHeaders['x-sdk-language']}; x-sdk-version: ${sdkHeaders['x-sdk-version']}`); + + const browserClient = Client.fromBrowser({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + endpointRealtime: 'wss://realtime.example.com/v1', + locale: 'en-US', + selfSigned: true, + }); + const browserHeaders = browserClient.getHeaders(); + console.log(browserHeaders['X-Appwrite-Project']); + console.log(browserHeaders['X-Appwrite-Locale']); + console.log(browserHeaders['x-sdk-platform']); + console.log(browserClient.config.endpointRealtime); + + const sessionClient = Client.fromSession({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + }); + console.log(sessionClient.getHeaders()['X-Appwrite-Session']); + + const devKeyClient = Client.fromDevKey({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + devKey: 'auth-dev-key', + }); + console.log(devKeyClient.getHeaders()['X-Appwrite-Dev-Key']); + + const impersonationUserClient = Client.fromImpersonation({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + userId: 'auth-user-id', + }); + console.log(impersonationUserClient.getHeaders()['X-Appwrite-Impersonate-User-Id']); + + const impersonationEmailClient = Client.fromImpersonation({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + email: 'auth@example.com', + }); + console.log(impersonationEmailClient.getHeaders()['X-Appwrite-Impersonate-User-Email']); + + const impersonationPhoneClient = Client.fromImpersonation({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + phone: '+15555550123', + }); + console.log(impersonationPhoneClient.getHeaders()['X-Appwrite-Impersonate-User-Phone']); + + try { + Client.fromImpersonation({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + }); + } catch (error) { + console.log(error.message); + } + + try { + Client.fromBrowser({ + endpoint: 'htp://cloud.appwrite.io/v1', + projectId: 'auth-project', + }); + } catch (error) { + console.log(error.message); + } + + try { + Client.fromBrowser({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + endpointRealtime: 'ftp://cloud.appwrite.io/v1', + }); + } catch (error) { + console.log(error.message); + } + client.setProject('123456'); response = await client.ping(); console.log(response.result); diff --git a/tests/languages/web/node.js b/tests/languages/web/node.js index 340a779a1b..d93733c091 100644 --- a/tests/languages/web/node.js +++ b/tests/languages/web/node.js @@ -13,6 +13,87 @@ async function start() { // Ping const sdkHeaders = client.getHeaders(); console.log(`x-sdk-name: ${sdkHeaders['x-sdk-name']}; x-sdk-platform: ${sdkHeaders['x-sdk-platform']}; x-sdk-language: ${sdkHeaders['x-sdk-language']}; x-sdk-version: ${sdkHeaders['x-sdk-version']}`); + + const browserClient = Client.fromBrowser({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + endpointRealtime: 'wss://realtime.example.com/v1', + locale: 'en-US', + selfSigned: true, + }); + const browserHeaders = browserClient.getHeaders(); + console.log(browserHeaders['X-Appwrite-Project']); + console.log(browserHeaders['X-Appwrite-Locale']); + console.log(browserHeaders['x-sdk-platform']); + console.log(browserClient.config.endpointRealtime); + + const sessionClient = Client.fromSession({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + }); + console.log(sessionClient.getHeaders()['X-Appwrite-Session']); + + const devKeyClient = Client.fromDevKey({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + devKey: 'auth-dev-key', + }); + console.log(devKeyClient.getHeaders()['X-Appwrite-Dev-Key']); + + const impersonationUserClient = Client.fromImpersonation({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + userId: 'auth-user-id', + }); + console.log(impersonationUserClient.getHeaders()['X-Appwrite-Impersonate-User-Id']); + + const impersonationEmailClient = Client.fromImpersonation({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + email: 'auth@example.com', + }); + console.log(impersonationEmailClient.getHeaders()['X-Appwrite-Impersonate-User-Email']); + + const impersonationPhoneClient = Client.fromImpersonation({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + phone: '+15555550123', + }); + console.log(impersonationPhoneClient.getHeaders()['X-Appwrite-Impersonate-User-Phone']); + + try { + Client.fromImpersonation({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + }); + } catch (error) { + console.log(error.message); + } + + try { + Client.fromBrowser({ + endpoint: 'htp://cloud.appwrite.io/v1', + projectId: 'auth-project', + }); + } catch (error) { + console.log(error.message); + } + + try { + Client.fromBrowser({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + endpointRealtime: 'ftp://cloud.appwrite.io/v1', + }); + } catch (error) { + console.log(error.message); + } + response = await client.ping(); console.log(response.result); From 3fa8c8cc593ccc393cc94c7fcb015438eaf55de7 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 12:06:59 +0530 Subject: [PATCH 42/69] Add Flutter auth factory test coverage --- tests/Base.php | 13 ++++++ tests/FlutterBetaTest.php | 1 + tests/FlutterStableTest.php | 1 + tests/languages/flutter/tests.dart | 74 ++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+) diff --git a/tests/Base.php b/tests/Base.php index 54e695ab22..6c238e6ead 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -150,6 +150,19 @@ abstract class Base extends TestCase 'Exactly one of userId, userEmail, or userPhone must be provided.', ]; + protected const FLUTTER_AUTH_FACTORY_RESPONSES = [ + 'auth-project', + 'en-US', + 'wss://realtime.example.com/v1', + 'auth-session', + 'auth-dev-key', + 'auth-user-id', + 'auth@example.com', + '+15555550123', + 'Exactly one of userId, userEmail, or userPhone must be provided.', + 'Invalid realtime endpoint URL: ftp://cloud.appwrite.io/v1', + ]; + protected const REALTIME_RESPONSES = [ 'WS:/v1/realtime:passed', 'WS:/v1/realtime:passed', diff --git a/tests/FlutterBetaTest.php b/tests/FlutterBetaTest.php index d7288b922a..a40271a6f7 100644 --- a/tests/FlutterBetaTest.php +++ b/tests/FlutterBetaTest.php @@ -19,6 +19,7 @@ class FlutterBetaTest extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/flutter ghcr.io/cirruslabs/flutter:beta sh -c "flutter pub get && flutter test test/appwrite_test.dart"'; protected array $expectedOutput = [ + ...Base::FLUTTER_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/FlutterStableTest.php b/tests/FlutterStableTest.php index 6c4f9a0ef7..8b0109afc4 100644 --- a/tests/FlutterStableTest.php +++ b/tests/FlutterStableTest.php @@ -19,6 +19,7 @@ class FlutterStableTest extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app:rw -w /app/tests/sdks/flutter ghcr.io/cirruslabs/flutter:stable sh -c "flutter pub get && flutter test test/appwrite_test.dart"'; protected array $expectedOutput = [ + ...Base::FLUTTER_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/languages/flutter/tests.dart b/tests/languages/flutter/tests.dart index 913ed43aaf..90f44a16b3 100644 --- a/tests/languages/flutter/tests.dart +++ b/tests/languages/flutter/tests.dart @@ -54,12 +54,86 @@ void main() async { ], ); + final authFactoryOutputs = []; + final browserClient = Client.fromBrowser( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + endPointRealtime: 'wss://realtime.example.com/v1', + locale: 'en-US', + selfSigned: true, + ); + authFactoryOutputs.add(browserClient.config['project']); + authFactoryOutputs.add(browserClient.config['locale']); + authFactoryOutputs.add(browserClient.endPointRealtime); + + final sessionClient = Client.fromSession( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + ); + authFactoryOutputs.add(sessionClient.config['session']); + + final devKeyClient = Client.fromDevKey( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + devKey: 'auth-dev-key', + ); + authFactoryOutputs.add(devKeyClient.config['devKey']); + + final impersonationUserClient = Client.fromImpersonation( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + userId: 'auth-user-id', + ); + authFactoryOutputs.add(impersonationUserClient.config['impersonateUserId']); + + final impersonationEmailClient = Client.fromImpersonation( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + userEmail: 'auth@example.com', + ); + authFactoryOutputs.add(impersonationEmailClient.config['impersonateUserEmail']); + + final impersonationPhoneClient = Client.fromImpersonation( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + userPhone: '+15555550123', + ); + authFactoryOutputs.add(impersonationPhoneClient.config['impersonateUserPhone']); + + try { + Client.fromImpersonation( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + ); + } on ArgumentError catch (e) { + authFactoryOutputs.add(e.message); + } + + try { + Client.fromBrowser( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + endPointRealtime: 'ftp://cloud.appwrite.io/v1', + ); + } on AppwriteException catch (e) { + authFactoryOutputs.add(e.message); + } + await Future.delayed(Duration(seconds: 5)); client.addHeader('Origin', 'http://localhost'); print('\nTest Started'); final sdkHeaders = client.getHeaders(); print("x-sdk-name: ${sdkHeaders['x-sdk-name']}; x-sdk-platform: ${sdkHeaders['x-sdk-platform']}; x-sdk-language: ${sdkHeaders['x-sdk-language']}; x-sdk-version: ${sdkHeaders['x-sdk-version']}"); + for (final output in authFactoryOutputs) { + print(output); + } + // Ping pong tests client.setProject('123456'); final ping = await client.ping(); From 2b3ed33df8d89883e4b214ca6fb8f62fa68d1818 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 12:19:19 +0530 Subject: [PATCH 43/69] Validate auth factory endpoint URLs --- .../src/main/java/io/package/Client.kt.twig | 9 ++++++++ templates/flutter/lib/src/client.dart.twig | 18 ++++++++++++++++ tests/Base.php | 3 +++ tests/languages/android/Tests.kt | 21 +++++++++++++++++++ tests/languages/flutter/tests.dart | 9 ++++++++ 5 files changed, 60 insertions(+) diff --git a/templates/android/library/src/main/java/io/package/Client.kt.twig b/templates/android/library/src/main/java/io/package/Client.kt.twig index d372facd14..31f0e48b9b 100644 --- a/templates/android/library/src/main/java/io/package/Client.kt.twig +++ b/templates/android/library/src/main/java/io/package/Client.kt.twig @@ -149,6 +149,15 @@ class Client @JvmOverloads constructor( locale: String?, selfSigned: Boolean ): Client { + require(endpoint.startsWith("http://") || endpoint.startsWith("https://")) { + "Invalid endpoint URL: $endpoint" + } + if (endpointRealtime != null) { + require(endpointRealtime.startsWith("ws://") || endpointRealtime.startsWith("wss://")) { + "Invalid realtime endpoint URL: $endpointRealtime" + } + } + val client = Client( context, endpoint, diff --git a/templates/flutter/lib/src/client.dart.twig b/templates/flutter/lib/src/client.dart.twig index 31b4516034..10a157b789 100644 --- a/templates/flutter/lib/src/client.dart.twig +++ b/templates/flutter/lib/src/client.dart.twig @@ -2,6 +2,7 @@ import 'enums.dart'; import 'client_stub.dart' if (dart.library.js_interop) 'client_browser.dart' if (dart.library.io) 'client_io.dart'; +import 'exception.dart'; import 'response.dart'; import 'upload_progress.dart'; @@ -77,6 +78,11 @@ abstract class Client implements ClientAuth { String? locale, required bool selfSigned, }) { + _validateEndpoint(endPoint); + if (endPointRealtime != null) { + _validateRealtimeEndpoint(endPointRealtime); + } + final client = createClient(endPoint: endPoint, selfSigned: selfSigned); _setHeader( @@ -101,6 +107,18 @@ abstract class Client implements ClientAuth { return client; } + static void _validateEndpoint(String endPoint) { + if (!endPoint.startsWith('http://') && !endPoint.startsWith('https://')) { + throw {{spec.title | caseUcfirst}}Exception('Invalid endpoint URL: $endPoint'); + } + } + + static void _validateRealtimeEndpoint(String endPointRealtime) { + if (!endPointRealtime.startsWith('ws://') && !endPointRealtime.startsWith('wss://')) { + throw {{spec.title | caseUcfirst}}Exception('Invalid realtime endpoint URL: $endPointRealtime'); + } + } + static void _setHeader( Client client, String configKey, diff --git a/tests/Base.php b/tests/Base.php index 6c238e6ead..6182f298fd 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -148,6 +148,8 @@ abstract class Base extends TestCase 'auth@example.com', '+15555550123', 'Exactly one of userId, userEmail, or userPhone must be provided.', + 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', + 'Invalid realtime endpoint URL: ftp://cloud.appwrite.io/v1', ]; protected const FLUTTER_AUTH_FACTORY_RESPONSES = [ @@ -160,6 +162,7 @@ abstract class Base extends TestCase 'auth@example.com', '+15555550123', 'Exactly one of userId, userEmail, or userPhone must be provided.', + 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', 'Invalid realtime endpoint URL: ftp://cloud.appwrite.io/v1', ]; diff --git a/tests/languages/android/Tests.kt b/tests/languages/android/Tests.kt index 67f16ce1c6..f96a2fd635 100644 --- a/tests/languages/android/Tests.kt +++ b/tests/languages/android/Tests.kt @@ -139,6 +139,27 @@ class ServiceTest { writeToFile(e.message) } + try { + Client.fromBrowser( + context = ApplicationProvider.getApplicationContext(), + projectId = "auth-project", + endpoint = "htp://cloud.appwrite.io/v1" + ) + } catch (e: IllegalArgumentException) { + writeToFile(e.message) + } + + try { + Client.fromBrowser( + context = ApplicationProvider.getApplicationContext(), + projectId = "auth-project", + endpoint = "https://cloud.appwrite.io/v1", + endpointRealtime = "ftp://cloud.appwrite.io/v1" + ) + } catch (e: IllegalArgumentException) { + writeToFile(e.message) + } + runBlocking { val ping = client.ping() val pingResponse = parse(ping) diff --git a/tests/languages/flutter/tests.dart b/tests/languages/flutter/tests.dart index 90f44a16b3..916e428d84 100644 --- a/tests/languages/flutter/tests.dart +++ b/tests/languages/flutter/tests.dart @@ -114,6 +114,15 @@ void main() async { authFactoryOutputs.add(e.message); } + try { + Client.fromBrowser( + endPoint: 'htp://cloud.appwrite.io/v1', + projectId: 'auth-project', + ); + } on AppwriteException catch (e) { + authFactoryOutputs.add(e.message); + } + try { Client.fromBrowser( endPoint: 'https://cloud.appwrite.io/v1', From e629feca58d56f577cfd22ed64c1d76517226c28 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 12:26:24 +0530 Subject: [PATCH 44/69] Enforce console cookie admin mode --- templates/web/src/client.ts.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 64fd2e5677..ac887240ef 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -448,7 +448,7 @@ class ClientRuntime { static fromCookie(params: Prettify): Client<'cookie'> { return new ClientRuntime<'cookie'>() {%~ if sdk.platform == 'console' %} - .applyBase<'cookie'>({ mode: 'admin', ...params }, 'server') + .applyBase<'cookie'>({ ...params, mode: 'admin' }, 'server') {%~ else %} .applyBase<'cookie'>(params, 'server') {%~ endif %} From f9e6a4788bbe5f5fab13d5ab2d506e9872cee4bf Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 13:40:26 +0530 Subject: [PATCH 45/69] Add auth factories to server SDK variants --- templates/dart/lib/src/client.dart.twig | 168 ++++++++++++++++++ .../main/kotlin/io/appwrite/Client.kt.twig | 161 +++++++++++++++++ templates/node/src/client.ts.twig | 97 ++++++++++ templates/react-native/src/client.ts.twig | 82 +++++++++ templates/swift/Sources/Client.swift.twig | 124 +++++++++++++ tests/Base.php | 64 +++++++ tests/DartBetaTest.php | 1 + tests/DartStableTest.php | 1 + tests/KotlinJava11Test.php | 1 + tests/KotlinJava17Test.php | 1 + tests/KotlinJava8Test.php | 1 + tests/Node16Test.php | 1 + tests/Node18Test.php | 1 + tests/Node20Test.php | 1 + tests/Swift56Test.php | 1 + tests/languages/dart/tests.dart | 93 ++++++++++ tests/languages/kotlin/Tests.kt | 92 ++++++++++ tests/languages/node/test.js | 90 ++++++++++ tests/languages/swift/Tests.swift | 91 ++++++++++ 19 files changed, 1071 insertions(+) diff --git a/templates/dart/lib/src/client.dart.twig b/templates/dart/lib/src/client.dart.twig index ebbe616eef..7627482db5 100644 --- a/templates/dart/lib/src/client.dart.twig +++ b/templates/dart/lib/src/client.dart.twig @@ -2,6 +2,7 @@ import 'enums.dart'; import 'client_stub.dart' if (dart.library.html) 'client_browser.dart' if (dart.library.io) 'client_io.dart'; +import 'exception.dart'; import 'response.dart'; import 'upload_progress.dart'; @@ -23,6 +24,173 @@ abstract class Client { bool selfSigned = false, }) => createClient(endPoint: endPoint, selfSigned: selfSigned); + /// Creates a client configured for browser-style authentication. + static Client fromBrowser({ + String endPoint = '{{ spec.endpoint }}', + required String projectId, + String? locale, + bool selfSigned = false, + }) => + _fromBase( + endPoint: endPoint, + projectId: projectId, + locale: locale, + selfSigned: selfSigned, + sdkPlatform: 'client', + ); + + /// Creates a client configured with a user session. + static Client fromSession({ + String endPoint = '{{ spec.endpoint }}', + required String projectId, + required String session, + String? locale, + bool selfSigned = false, + }) { + final client = _fromBase( + endPoint: endPoint, + projectId: projectId, + locale: locale, + selfSigned: selfSigned, + sdkPlatform: 'client', + ); + client.setSession(session); + return client; + } + + /// Creates a client configured with an API key. + static Client fromAPIKey({ + String endPoint = '{{ spec.endpoint }}', + required String projectId, + required String apiKey, + String? locale, + bool selfSigned = false, + }) { + final client = _fromBase( + endPoint: endPoint, + projectId: projectId, + locale: locale, + selfSigned: selfSigned, + sdkPlatform: 'server', + ); + client.setKey(apiKey); + return client; + } + + /// Creates a client configured with a cookie header. + static Client fromCookie({ + String endPoint = '{{ spec.endpoint }}', + required String projectId, + required String cookie, + String? locale, + bool selfSigned = false, + }) { + final client = _fromBase( + endPoint: endPoint, + projectId: projectId, + locale: locale, + selfSigned: selfSigned, + sdkPlatform: 'server', + ); + client.addHeader('Cookie', cookie); + return client; + } + + /// Creates a client configured with a JWT. + static Client fromJWT({ + String endPoint = '{{ spec.endpoint }}', + required String projectId, + required String jwt, + String? locale, + bool selfSigned = false, + }) { + final client = _fromBase( + endPoint: endPoint, + projectId: projectId, + locale: locale, + selfSigned: selfSigned, + sdkPlatform: 'server', + ); + client.setJWT(jwt); + return client; + } + + /// Creates a client configured with a development key. + static Client fromDevKey({ + String endPoint = '{{ spec.endpoint }}', + required String projectId, + required String devKey, + String? locale, + bool selfSigned = false, + }) { + final client = _fromBase( + endPoint: endPoint, + projectId: projectId, + locale: locale, + selfSigned: selfSigned, + sdkPlatform: 'client', + ); + client.addHeader('X-{{ spec.title | caseUcfirst }}-Dev-Key', devKey); + return client; + } + + /// Creates a client configured for impersonation. + static Client fromImpersonation({ + String endPoint = '{{ spec.endpoint }}', + required String projectId, + required String session, + String? userId, + String? userEmail, + String? userPhone, + String? locale, + bool selfSigned = false, + }) { + final targets = [userId, userEmail, userPhone].whereType().length; + if (targets != 1) { + throw {{spec.title | caseUcfirst}}Exception( + 'Exactly one of userId, userEmail, or userPhone must be provided.', + ); + } + + final client = _fromBase( + endPoint: endPoint, + projectId: projectId, + locale: locale, + selfSigned: selfSigned, + sdkPlatform: 'client', + ); + client.setSession(session); + + if (userId != null) { + client.addHeader('X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id', userId); + } else if (userEmail != null) { + client.addHeader('X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email', userEmail); + } else { + client.addHeader('X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone', userPhone!); + } + + return client; + } + + static Client _fromBase({ + required String endPoint, + required String projectId, + String? locale, + required bool selfSigned, + required String sdkPlatform, + }) { + final client = Client(selfSigned: selfSigned) + ..setEndpoint(endPoint) + ..setProject(projectId) + ..addHeader('x-sdk-platform', sdkPlatform); + + if (locale != null) { + client.setLocale(locale); + } + + return client; + } + /// Handle OAuth2 session creation. Future webAuth(Uri url); diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig index 17992731c0..f4b6a96d80 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig @@ -39,6 +39,167 @@ class Client @JvmOverloads constructor( companion object { const val CHUNK_SIZE = 5*1024*1024; // 5MB + + @JvmStatic + @JvmOverloads + fun fromBrowser( + projectId: String, + endPoint: String = "{{spec.endpoint}}", + locale: String? = null, + selfSigned: Boolean = false + ): Client { + return fromBase( + endPoint = endPoint, + projectId = projectId, + locale = locale, + selfSigned = selfSigned, + sdkPlatform = "client" + ) + } + + @JvmStatic + @JvmOverloads + fun fromSession( + projectId: String, + session: String, + endPoint: String = "{{spec.endpoint}}", + locale: String? = null, + selfSigned: Boolean = false + ): Client { + return fromBrowser( + projectId = projectId, + endPoint = endPoint, + locale = locale, + selfSigned = selfSigned + ).setSession(session) + } + + @JvmStatic + @JvmOverloads + fun fromAPIKey( + projectId: String, + apiKey: String, + endPoint: String = "{{spec.endpoint}}", + locale: String? = null, + selfSigned: Boolean = false + ): Client { + return fromBase( + endPoint = endPoint, + projectId = projectId, + locale = locale, + selfSigned = selfSigned, + sdkPlatform = "server" + ).setKey(apiKey) + } + + @JvmStatic + @JvmOverloads + fun fromCookie( + projectId: String, + cookie: String, + endPoint: String = "{{spec.endpoint}}", + locale: String? = null, + selfSigned: Boolean = false + ): Client { + val client = fromBase( + endPoint = endPoint, + projectId = projectId, + locale = locale, + selfSigned = selfSigned, + sdkPlatform = "server" + ) + client.addHeader("Cookie", cookie) + return client + } + + @JvmStatic + @JvmOverloads + fun fromJWT( + projectId: String, + jwt: String, + endPoint: String = "{{spec.endpoint}}", + locale: String? = null, + selfSigned: Boolean = false + ): Client { + return fromBase( + endPoint = endPoint, + projectId = projectId, + locale = locale, + selfSigned = selfSigned, + sdkPlatform = "server" + ).setJWT(jwt) + } + + @JvmStatic + @JvmOverloads + fun fromDevKey( + projectId: String, + devKey: String, + endPoint: String = "{{spec.endpoint}}", + locale: String? = null, + selfSigned: Boolean = false + ): Client { + val client = fromBrowser( + projectId = projectId, + endPoint = endPoint, + locale = locale, + selfSigned = selfSigned + ) + client.addHeader("X-{{ spec.title | caseUcfirst }}-Dev-Key", devKey) + return client + } + + @JvmStatic + @JvmOverloads + fun fromImpersonation( + projectId: String, + session: String, + userId: String? = null, + userEmail: String? = null, + userPhone: String? = null, + endPoint: String = "{{spec.endpoint}}", + locale: String? = null, + selfSigned: Boolean = false + ): Client { + val targets = listOfNotNull(userId, userEmail, userPhone).size + require(targets == 1) { + "Exactly one of userId, userEmail, or userPhone must be provided." + } + + val client = fromBrowser( + projectId = projectId, + endPoint = endPoint, + locale = locale, + selfSigned = selfSigned + ).setSession(session) + + when { + userId != null -> client.addHeader("X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id", userId) + userEmail != null -> client.addHeader("X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email", userEmail) + userPhone != null -> client.addHeader("X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone", userPhone) + } + + return client + } + + private fun fromBase( + endPoint: String, + projectId: String, + locale: String?, + selfSigned: Boolean, + sdkPlatform: String + ): Client { + return Client() + .setEndpoint(endPoint) + .setSelfSigned(selfSigned) + .setProject(projectId) + .addHeader("x-sdk-platform", sdkPlatform) + .also { client -> + if (locale != null) { + client.setLocale(locale) + } + } + } } override val coroutineContext: CoroutineContext diff --git a/templates/node/src/client.ts.twig b/templates/node/src/client.ts.twig index 8a3e3ba684..8df26c76a2 100644 --- a/templates/node/src/client.ts.twig +++ b/templates/node/src/client.ts.twig @@ -59,6 +59,21 @@ type Headers = { [key: string]: string; } +type SDKPlatform = 'client' | 'server'; + +type BaseClientParams = { + endpoint: string; + selfSigned?: boolean; +{%~ for param in spec.global.headers | webClientBaseParams %} + {{ param.name }}{% if not param.required %}?{% endif %}: string; +{%~ endfor %} +}; + +type ImpersonationTarget = + | { userId: string; email?: never; phone?: never } + | { email: string; userId?: never; phone?: never } + | { phone: string; userId?: never; email?: never }; + class {{spec.title | caseUcfirst}}Exception extends Error { code: number; response: string; @@ -127,6 +142,88 @@ class Client { {%~ endfor %} }; + static fromBrowser(params: BaseClientParams): Client { + return new Client().applyBase(params, 'client'); + } + + static fromSession(params: BaseClientParams & { session: string }): Client { + return new Client() + .applyBase(params, 'client') + .setSession(params.session); + } + + static fromAPIKey(params: BaseClientParams & { apiKey: string }): Client { + return new Client() + .applyBase(params, 'server') + .setKey(params.apiKey); + } + + static fromCookie(params: BaseClientParams & { cookie: string }): Client { + const client = new Client().applyBase(params, 'server'); + client.headers['Cookie'] = params.cookie; + return client; + } + + static fromJWT(params: BaseClientParams & { jwt: string }): Client { + return new Client() + .applyBase(params, 'server') + .setJWT(params.jwt); + } + + static fromDevKey(params: BaseClientParams & { devKey: string }): Client { + const client = new Client().applyBase(params, 'client'); + client.headers['X-{{ spec.title | caseUcfirst }}-Dev-Key'] = params.devKey; + return client; + } + + static fromImpersonation(params: BaseClientParams & { session: string } & ImpersonationTarget): Client { + const targets = [ + params.userId !== undefined, + params.email !== undefined, + params.phone !== undefined + ].filter(Boolean).length; + + if (targets !== 1) { + throw new {{spec.title | caseUcfirst}}Exception('Exactly one impersonation target must be provided'); + } + + const client = new Client() + .applyBase(params, 'client') + .setSession(params.session); + + if (params.userId !== undefined) { + client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id'] = params.userId; + return client; + } + if (params.email !== undefined) { + client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email'] = params.email; + return client; + } + client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone'] = params.phone; + return client; + } + + private applyBase(params: BaseClientParams, sdkPlatform: SDKPlatform): Client { + this.headers['x-sdk-platform'] = sdkPlatform; + this.setEndpoint(params.endpoint); +{%~ for param in spec.global.headers | webClientBaseParams %} +{%~ if param.required %} + this.{{ param.setter }}(params.{{ param.name }}); + +{%~ else %} + if (params.{{ param.name }} !== undefined) { + this.{{ param.setter }}(params.{{ param.name }}); + } + +{%~ endif %} +{%~ endfor %} + if (params.selfSigned !== undefined) { + this.setSelfSigned(params.selfSigned); + } + + return this; + } + /** * Set Endpoint * diff --git a/templates/react-native/src/client.ts.twig b/templates/react-native/src/client.ts.twig index f4bdb85a2a..180b081b59 100644 --- a/templates/react-native/src/client.ts.twig +++ b/templates/react-native/src/client.ts.twig @@ -51,6 +51,19 @@ type Headers = { [key: string]: string; } +type BaseClientParams = { + endpoint: string; + endpointRealtime?: string; +{%~ for param in spec.global.headers | webClientBaseParams %} + {{ param.name }}{% if not param.required %}?{% endif %}: string; +{%~ endfor %} +}; + +type ImpersonationTarget = + | { userId: string; email?: never; phone?: never } + | { email: string; userId?: never; phone?: never } + | { phone: string; userId?: never; email?: never }; + type RealtimeResponse = { type: 'error' | 'event' | 'connected' | 'response' | 'pong'; data: RealtimeResponseAuthenticated | RealtimeResponseConnected | RealtimeResponseError | RealtimeResponseEvent | undefined; @@ -182,6 +195,75 @@ class Client { {% endfor %} }; + static fromBrowser(params: BaseClientParams): Client { + return new Client().applyBase(params); + } + + static fromSession(params: BaseClientParams & { session: string }): Client { + return new Client() + .applyBase(params) + .setSession(params.session); + } + + static fromCookie(params: BaseClientParams & { cookie: string }): Client { + const client = new Client().applyBase(params); + client.headers['Cookie'] = params.cookie; + return client; + } + + static fromDevKey(params: BaseClientParams & { devKey: string }): Client { + const client = new Client().applyBase(params); + client.headers['X-{{ spec.title | caseUcfirst }}-Dev-Key'] = params.devKey; + return client; + } + + static fromImpersonation(params: BaseClientParams & { session: string } & ImpersonationTarget): Client { + const targets = [ + params.userId !== undefined, + params.email !== undefined, + params.phone !== undefined + ].filter(Boolean).length; + + if (targets !== 1) { + throw new {{spec.title | caseUcfirst}}Exception('Exactly one impersonation target must be provided'); + } + + const client = new Client() + .applyBase(params) + .setSession(params.session); + + if (params.userId !== undefined) { + client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id'] = params.userId; + } else if (params.email !== undefined) { + client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email'] = params.email; + } else { + client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone'] = params.phone; + } + + return client; + } + + private applyBase(params: BaseClientParams): Client { + this.headers['x-sdk-platform'] = 'client'; + this.setEndpoint(params.endpoint); +{%~ for param in spec.global.headers | webClientBaseParams %} +{%~ if param.required %} + this.{{ param.setter }}(params.{{ param.name }}); + +{%~ else %} + if (params.{{ param.name }} !== undefined) { + this.{{ param.setter }}(params.{{ param.name }}); + } + +{%~ endif %} +{%~ endfor %} + if (params.endpointRealtime !== undefined) { + this.setEndpointRealtime(params.endpointRealtime); + } + + return this; + } + /** * Get Headers * diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index 100a00aeb4..83857afac0 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -55,6 +55,130 @@ open class Client { addOriginHeader() } + public static func fromBrowser( + endpoint: String = "{{spec.endpoint}}", + projectId: String, + locale: String? = nil, + selfSigned: Bool = false + ) throws -> Client { + let client = Client() + try configure(client, endpoint: endpoint, projectId: projectId, locale: locale, selfSigned: selfSigned, sdkPlatform: "client") + return client + } + + public static func fromSession( + endpoint: String = "{{spec.endpoint}}", + projectId: String, + session: String, + locale: String? = nil, + selfSigned: Bool = false + ) throws -> Client { + let client = try fromBrowser(endpoint: endpoint, projectId: projectId, locale: locale, selfSigned: selfSigned) + client.setSession(session) + return client + } + + public static func fromAPIKey( + endpoint: String = "{{spec.endpoint}}", + projectId: String, + apiKey: String, + locale: String? = nil, + selfSigned: Bool = false + ) throws -> Client { + let client = Client() + try configure(client, endpoint: endpoint, projectId: projectId, locale: locale, selfSigned: selfSigned, sdkPlatform: "server") + client.setKey(apiKey) + return client + } + + public static func fromCookie( + endpoint: String = "{{spec.endpoint}}", + projectId: String, + cookie: String, + locale: String? = nil, + selfSigned: Bool = false + ) throws -> Client { + let client = Client() + try configure(client, endpoint: endpoint, projectId: projectId, locale: locale, selfSigned: selfSigned, sdkPlatform: "server") + client.headers["Cookie"] = cookie + return client + } + + public static func fromJWT( + endpoint: String = "{{spec.endpoint}}", + projectId: String, + jwt: String, + locale: String? = nil, + selfSigned: Bool = false + ) throws -> Client { + let client = Client() + try configure(client, endpoint: endpoint, projectId: projectId, locale: locale, selfSigned: selfSigned, sdkPlatform: "server") + client.setJWT(jwt) + return client + } + + public static func fromDevKey( + endpoint: String = "{{spec.endpoint}}", + projectId: String, + devKey: String, + locale: String? = nil, + selfSigned: Bool = false + ) throws -> Client { + let client = try fromBrowser(endpoint: endpoint, projectId: projectId, locale: locale, selfSigned: selfSigned) + client.headers["X-{{ spec.title | caseUcfirst }}-Dev-Key"] = devKey + return client + } + + public static func fromImpersonation( + endpoint: String = "{{spec.endpoint}}", + projectId: String, + session: String, + userId: String? = nil, + userEmail: String? = nil, + userPhone: String? = nil, + locale: String? = nil, + selfSigned: Bool = false + ) throws -> Client { + let targetCount = [userId, userEmail, userPhone].compactMap { $0 }.count + guard targetCount == 1 else { + throw {{ spec.title | caseUcfirst }}Error(message: "Provide exactly one impersonation target.") + } + + let client = try fromBrowser(endpoint: endpoint, projectId: projectId, locale: locale, selfSigned: selfSigned) + client.setSession(session) + + if let userId = userId { + client.headers["X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id"] = userId + } else if let userEmail = userEmail { + client.headers["X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email"] = userEmail + } else if let userPhone = userPhone { + client.headers["X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone"] = userPhone + } + + return client + } + + private static func configure( + _ client: Client, + endpoint: String, + projectId: String, + locale: String?, + selfSigned: Bool, + sdkPlatform: String + ) throws { + guard endpoint.hasPrefix("http://") || endpoint.hasPrefix("https://") else { + throw {{ spec.title | caseUcfirst }}Error(message: "Invalid endpoint URL: \(endpoint)") + } + + client.endPoint = endpoint + client.headers["x-sdk-platform"] = sdkPlatform + client.setProject(projectId) + if let locale = locale { + client.setLocale(locale) + } + client.setSelfSigned(selfSigned) + } + private static func createHTTP( selfSigned: Bool = false, redirectConfiguration: HTTPClient.Configuration.RedirectConfiguration = .follow(max: 5, allowCycles: false), diff --git a/tests/Base.php b/tests/Base.php index 6182f298fd..80022f0fe2 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -124,6 +124,70 @@ abstract class Base extends TestCase 'Invalid realtime endpoint URL: ftp://cloud.appwrite.io/v1', ]; + protected const NODE_AUTH_FACTORY_RESPONSES = [ + 'auth-project', + 'en-US', + 'client', + 'auth-session', + 'auth-api-key', + 'auth-cookie', + 'auth-jwt', + 'auth-dev-key', + 'auth-user-id', + 'auth@example.com', + '+15555550123', + 'Exactly one impersonation target must be provided', + 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', + ]; + + protected const DART_AUTH_FACTORY_RESPONSES = [ + 'auth-project', + 'en-US', + 'client', + 'auth-session', + 'auth-api-key', + 'auth-cookie', + 'auth-jwt', + 'auth-dev-key', + 'auth-user-id', + 'auth@example.com', + '+15555550123', + 'Exactly one of userId, userEmail, or userPhone must be provided.', + 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', + ]; + + protected const SWIFT_AUTH_FACTORY_RESPONSES = [ + 'auth-project', + 'en-US', + 'client', + 'auth-session', + 'auth-api-key', + 'auth-cookie', + 'auth-jwt', + 'auth-dev-key', + 'auth-user-id', + 'auth@example.com', + '+15555550123', + 'Provide exactly one impersonation target.', + 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', + ]; + + protected const KOTLIN_AUTH_FACTORY_RESPONSES = [ + 'auth-project', + 'en-US', + 'client', + 'auth-session', + 'auth-api-key', + 'auth-cookie', + 'auth-jwt', + 'auth-dev-key', + 'auth-user-id', + 'auth@example.com', + '+15555550123', + 'Exactly one of userId, userEmail, or userPhone must be provided.', + 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', + ]; + protected const APPLE_AUTH_FACTORY_RESPONSES = [ 'auth-project', 'en-US', diff --git a/tests/DartBetaTest.php b/tests/DartBetaTest.php index 2c160b68cf..4f83344f83 100644 --- a/tests/DartBetaTest.php +++ b/tests/DartBetaTest.php @@ -19,6 +19,7 @@ class DartBetaTest extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/dart dart:beta sh -c "dart pub get && dart pub run tests/tests.dart"'; protected array $expectedOutput = [ + ...Base::DART_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/DartStableTest.php b/tests/DartStableTest.php index 2778ee8770..6b93f8dac0 100644 --- a/tests/DartStableTest.php +++ b/tests/DartStableTest.php @@ -19,6 +19,7 @@ class DartStableTest extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/dart dart:stable sh -c "dart pub get && dart pub run tests/tests.dart"'; protected array $expectedOutput = [ + ...Base::DART_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/KotlinJava11Test.php b/tests/KotlinJava11Test.php index 5a2e76ef67..5c674b4707 100644 --- a/tests/KotlinJava11Test.php +++ b/tests/KotlinJava11Test.php @@ -20,6 +20,7 @@ class KotlinJava11Test extends Base 'docker run --network="mockapi" -v $(pwd):/app -w /app/tests/sdks/kotlin eclipse-temurin:11-jdk-jammy sh -c "./gradlew test -q && cat result.txt"'; protected array $expectedOutput = [ + ...Base::KOTLIN_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/KotlinJava17Test.php b/tests/KotlinJava17Test.php index 7339c16b32..d63cb118aa 100644 --- a/tests/KotlinJava17Test.php +++ b/tests/KotlinJava17Test.php @@ -20,6 +20,7 @@ class KotlinJava17Test extends Base 'docker run --network="mockapi" -v $(pwd):/app -w /app/tests/sdks/kotlin eclipse-temurin:17-jdk-jammy sh -c "./gradlew test -q && cat result.txt"'; protected array $expectedOutput = [ + ...Base::KOTLIN_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/KotlinJava8Test.php b/tests/KotlinJava8Test.php index 6243b94ef7..aa7901b86a 100644 --- a/tests/KotlinJava8Test.php +++ b/tests/KotlinJava8Test.php @@ -20,6 +20,7 @@ class KotlinJava8Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/kotlin eclipse-temurin:8-jdk-jammy sh -c "./gradlew test -q && cat result.txt"'; protected array $expectedOutput = [ + ...Base::KOTLIN_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/Node16Test.php b/tests/Node16Test.php index d5ab3f3674..7f8af1d792 100644 --- a/tests/Node16Test.php +++ b/tests/Node16Test.php @@ -20,6 +20,7 @@ class Node16Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app node:16-alpine node tests/sdks/node/test.js'; protected array $expectedOutput = [ + ...Base::NODE_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::FOO_RESPONSES, // Object params diff --git a/tests/Node18Test.php b/tests/Node18Test.php index 138411d733..7ef04e6427 100644 --- a/tests/Node18Test.php +++ b/tests/Node18Test.php @@ -20,6 +20,7 @@ class Node18Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app node:18-alpine node tests/sdks/node/test.js'; protected array $expectedOutput = [ + ...Base::NODE_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::FOO_RESPONSES, // Object params diff --git a/tests/Node20Test.php b/tests/Node20Test.php index 280c34ed3e..0cc4356ab1 100644 --- a/tests/Node20Test.php +++ b/tests/Node20Test.php @@ -20,6 +20,7 @@ class Node20Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app node:20-alpine node tests/sdks/node/test.js'; protected array $expectedOutput = [ + ...Base::NODE_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::FOO_RESPONSES, // Object params diff --git a/tests/Swift56Test.php b/tests/Swift56Test.php index 697a99d0c3..8d3a731eac 100644 --- a/tests/Swift56Test.php +++ b/tests/Swift56Test.php @@ -19,6 +19,7 @@ class Swift56Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/swift swift:5.6-focal swift test'; protected array $expectedOutput = [ + ...Base::SWIFT_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/languages/dart/tests.dart b/tests/languages/dart/tests.dart index 0250217260..02f04cf8a3 100644 --- a/tests/languages/dart/tests.dart +++ b/tests/languages/dart/tests.dart @@ -7,6 +7,96 @@ import '../lib/packageName.dart'; import '../lib/src/input_file.dart'; void main() async { + final authFactoryOutputs = []; + final browserClient = Client.fromBrowser( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + locale: 'en-US', + selfSigned: true, + ); + final browserHeaders = browserClient.getHeaders(); + authFactoryOutputs.add(browserHeaders['X-Appwrite-Project']); + authFactoryOutputs.add(browserHeaders['X-Appwrite-Locale']); + authFactoryOutputs.add(browserHeaders['x-sdk-platform']); + + final sessionClient = Client.fromSession( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + ); + authFactoryOutputs.add(sessionClient.getHeaders()['X-Appwrite-Session']); + + final apiKeyClient = Client.fromAPIKey( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + apiKey: 'auth-api-key', + ); + authFactoryOutputs.add(apiKeyClient.getHeaders()['X-Appwrite-Key']); + + final cookieClient = Client.fromCookie( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + cookie: 'auth-cookie', + ); + authFactoryOutputs.add(cookieClient.getHeaders()['Cookie']); + + final jwtClient = Client.fromJWT( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + jwt: 'auth-jwt', + ); + authFactoryOutputs.add(jwtClient.getHeaders()['X-Appwrite-JWT']); + + final devKeyClient = Client.fromDevKey( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + devKey: 'auth-dev-key', + ); + authFactoryOutputs.add(devKeyClient.getHeaders()['X-Appwrite-Dev-Key']); + + final impersonationUserClient = Client.fromImpersonation( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + userId: 'auth-user-id', + ); + authFactoryOutputs.add(impersonationUserClient.getHeaders()['X-Appwrite-Impersonate-User-Id']); + + final impersonationEmailClient = Client.fromImpersonation( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + userEmail: 'auth@example.com', + ); + authFactoryOutputs.add(impersonationEmailClient.getHeaders()['X-Appwrite-Impersonate-User-Email']); + + final impersonationPhoneClient = Client.fromImpersonation( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + userPhone: '+15555550123', + ); + authFactoryOutputs.add(impersonationPhoneClient.getHeaders()['X-Appwrite-Impersonate-User-Phone']); + + try { + Client.fromImpersonation( + endPoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + ); + } on AppwriteException catch (e) { + authFactoryOutputs.add(e.message); + } + + try { + Client.fromBrowser( + endPoint: 'htp://cloud.appwrite.io/v1', + projectId: 'auth-project', + ); + } on AppwriteException catch (e) { + authFactoryOutputs.add(e.message); + } + Client client = Client().setSelfSigned(); Foo foo = Foo(client); Bar bar = Bar(client); @@ -18,6 +108,9 @@ void main() async { print('\nTest Started'); final sdkHeaders = client.getHeaders(); print("x-sdk-name: ${sdkHeaders['x-sdk-name']}; x-sdk-platform: ${sdkHeaders['x-sdk-platform']}; x-sdk-language: ${sdkHeaders['x-sdk-language']}; x-sdk-version: ${sdkHeaders['x-sdk-version']}"); + for (final output in authFactoryOutputs) { + print(output); + } // Ping pong test client.setProject('123456'); diff --git a/tests/languages/kotlin/Tests.kt b/tests/languages/kotlin/Tests.kt index 6472862d27..3d52bd1159 100644 --- a/tests/languages/kotlin/Tests.kt +++ b/tests/languages/kotlin/Tests.kt @@ -40,6 +40,97 @@ class ServiceTest { @Test @Throws(IOException::class) fun test() { + val browserClient = Client.fromBrowser( + projectId = "auth-project", + endPoint = "https://cloud.appwrite.io/v1", + locale = "en-US", + selfSigned = true + ) + val browserHeaders = browserClient.getHeaders() + val authFactoryOutputs = mutableListOf( + browserHeaders["x-appwrite-project"], + browserHeaders["x-appwrite-locale"], + browserHeaders["x-sdk-platform"] + ) + + val sessionClient = Client.fromSession( + projectId = "auth-project", + session = "auth-session", + endPoint = "https://cloud.appwrite.io/v1" + ) + authFactoryOutputs.add(sessionClient.getHeaders()["x-appwrite-session"]) + + val apiKeyClient = Client.fromAPIKey( + projectId = "auth-project", + apiKey = "auth-api-key", + endPoint = "https://cloud.appwrite.io/v1" + ) + authFactoryOutputs.add(apiKeyClient.getHeaders()["x-appwrite-key"]) + + val cookieClient = Client.fromCookie( + projectId = "auth-project", + cookie = "auth-cookie", + endPoint = "https://cloud.appwrite.io/v1" + ) + authFactoryOutputs.add(cookieClient.getHeaders()["Cookie"]) + + val jwtClient = Client.fromJWT( + projectId = "auth-project", + jwt = "auth-jwt", + endPoint = "https://cloud.appwrite.io/v1" + ) + authFactoryOutputs.add(jwtClient.getHeaders()["x-appwrite-jwt"]) + + val devKeyClient = Client.fromDevKey( + projectId = "auth-project", + devKey = "auth-dev-key", + endPoint = "https://cloud.appwrite.io/v1" + ) + authFactoryOutputs.add(devKeyClient.getHeaders()["X-Appwrite-Dev-Key"]) + + val impersonationUserClient = Client.fromImpersonation( + projectId = "auth-project", + session = "auth-session", + userId = "auth-user-id", + endPoint = "https://cloud.appwrite.io/v1" + ) + authFactoryOutputs.add(impersonationUserClient.getHeaders()["X-Appwrite-Impersonate-User-Id"]) + + val impersonationEmailClient = Client.fromImpersonation( + projectId = "auth-project", + session = "auth-session", + userEmail = "auth@example.com", + endPoint = "https://cloud.appwrite.io/v1" + ) + authFactoryOutputs.add(impersonationEmailClient.getHeaders()["X-Appwrite-Impersonate-User-Email"]) + + val impersonationPhoneClient = Client.fromImpersonation( + projectId = "auth-project", + session = "auth-session", + userPhone = "+15555550123", + endPoint = "https://cloud.appwrite.io/v1" + ) + authFactoryOutputs.add(impersonationPhoneClient.getHeaders()["X-Appwrite-Impersonate-User-Phone"]) + + try { + Client.fromImpersonation( + projectId = "auth-project", + session = "auth-session", + endPoint = "https://cloud.appwrite.io/v1" + ) + } catch (e: IllegalArgumentException) { + authFactoryOutputs.add(e.message) + } + + try { + Client.fromBrowser( + projectId = "auth-project", + endPoint = "htp://cloud.appwrite.io/v1" + ) + } catch (e: IllegalArgumentException) { + authFactoryOutputs.add(e.message) + } + val client = Client() .setProject("123456") .addHeader("Origin", "http://localhost") @@ -47,6 +138,7 @@ class ServiceTest { val sdkHeaders = client.getHeaders() writeToFile("x-sdk-name: ${sdkHeaders["x-sdk-name"]}; x-sdk-platform: ${sdkHeaders["x-sdk-platform"]}; x-sdk-language: ${sdkHeaders["x-sdk-language"]}; x-sdk-version: ${sdkHeaders["x-sdk-version"]}") + authFactoryOutputs.forEach { writeToFile(it ?: "null") } runBlocking { val ping = client.ping() diff --git a/tests/languages/node/test.js b/tests/languages/node/test.js index e51a3058b7..7c933412b6 100644 --- a/tests/languages/node/test.js +++ b/tests/languages/node/test.js @@ -17,6 +17,95 @@ const { readFile } = require('fs/promises'); async function start() { let response; + const authFactoryOutputs = []; + const browserClient = Client.fromBrowser({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + locale: 'en-US' + }); + const browserHeaders = browserClient.getHeaders(); + authFactoryOutputs.push(browserHeaders['X-Appwrite-Project']); + authFactoryOutputs.push(browserHeaders['X-Appwrite-Locale']); + authFactoryOutputs.push(browserHeaders['x-sdk-platform']); + + const sessionClient = Client.fromSession({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session' + }); + authFactoryOutputs.push(sessionClient.getHeaders()['X-Appwrite-Session']); + + const apiKeyClient = Client.fromAPIKey({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + apiKey: 'auth-api-key' + }); + authFactoryOutputs.push(apiKeyClient.getHeaders()['X-Appwrite-Key']); + + const cookieClient = Client.fromCookie({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + cookie: 'auth-cookie' + }); + authFactoryOutputs.push(cookieClient.getHeaders()['Cookie']); + + const jwtClient = Client.fromJWT({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + jwt: 'auth-jwt' + }); + authFactoryOutputs.push(jwtClient.getHeaders()['X-Appwrite-JWT']); + + const devKeyClient = Client.fromDevKey({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + devKey: 'auth-dev-key' + }); + authFactoryOutputs.push(devKeyClient.getHeaders()['X-Appwrite-Dev-Key']); + + const impersonationUserClient = Client.fromImpersonation({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + userId: 'auth-user-id' + }); + authFactoryOutputs.push(impersonationUserClient.getHeaders()['X-Appwrite-Impersonate-User-Id']); + + const impersonationEmailClient = Client.fromImpersonation({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + email: 'auth@example.com' + }); + authFactoryOutputs.push(impersonationEmailClient.getHeaders()['X-Appwrite-Impersonate-User-Email']); + + const impersonationPhoneClient = Client.fromImpersonation({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session', + phone: '+15555550123' + }); + authFactoryOutputs.push(impersonationPhoneClient.getHeaders()['X-Appwrite-Impersonate-User-Phone']); + + try { + Client.fromImpersonation({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + session: 'auth-session' + }); + } catch (error) { + authFactoryOutputs.push(error.message); + } + + try { + Client.fromBrowser({ + endpoint: 'htp://cloud.appwrite.io/v1', + projectId: 'auth-project' + }); + } catch (error) { + authFactoryOutputs.push(error.message); + } + // Init SDK const client = new Client() .addHeader("Origin", "http://localhost") @@ -32,6 +121,7 @@ async function start() { console.log('\nTest Started'); const sdkHeaders = client.getHeaders(); console.log(`x-sdk-name: ${sdkHeaders['x-sdk-name']}; x-sdk-platform: ${sdkHeaders['x-sdk-platform']}; x-sdk-language: ${sdkHeaders['x-sdk-language']}; x-sdk-version: ${sdkHeaders['x-sdk-version']}`); + authFactoryOutputs.forEach(output => console.log(output)); // Ping response = await client.ping(); diff --git a/tests/languages/swift/Tests.swift b/tests/languages/swift/Tests.swift index afd05e6708..7a682a3688 100644 --- a/tests/languages/swift/Tests.swift +++ b/tests/languages/swift/Tests.swift @@ -20,12 +20,103 @@ class Tests: XCTestCase { func test() async throws { do { + var authFactoryOutputs: [String] = [] + let browserClient = try Client.fromBrowser( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + locale: "en-US", + selfSigned: true + ) + let browserHeaders = browserClient.getHeaders() + authFactoryOutputs.append(browserHeaders["X-Appwrite-Project"] ?? "nil") + authFactoryOutputs.append(browserHeaders["X-Appwrite-Locale"] ?? "nil") + authFactoryOutputs.append(browserHeaders["x-sdk-platform"] ?? "nil") + + let sessionClient = try Client.fromSession( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + session: "auth-session" + ) + authFactoryOutputs.append(sessionClient.getHeaders()["X-Appwrite-Session"] ?? "nil") + + let apiKeyClient = try Client.fromAPIKey( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + apiKey: "auth-api-key" + ) + authFactoryOutputs.append(apiKeyClient.getHeaders()["X-Appwrite-Key"] ?? "nil") + + let cookieClient = try Client.fromCookie( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + cookie: "auth-cookie" + ) + authFactoryOutputs.append(cookieClient.getHeaders()["Cookie"] ?? "nil") + + let jwtClient = try Client.fromJWT( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + jwt: "auth-jwt" + ) + authFactoryOutputs.append(jwtClient.getHeaders()["X-Appwrite-JWT"] ?? "nil") + + let devKeyClient = try Client.fromDevKey( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + devKey: "auth-dev-key" + ) + authFactoryOutputs.append(devKeyClient.getHeaders()["X-Appwrite-Dev-Key"] ?? "nil") + + let impersonationUserClient = try Client.fromImpersonation( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + session: "auth-session", + userId: "auth-user-id" + ) + authFactoryOutputs.append(impersonationUserClient.getHeaders()["X-Appwrite-Impersonate-User-Id"] ?? "nil") + + let impersonationEmailClient = try Client.fromImpersonation( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + session: "auth-session", + userEmail: "auth@example.com" + ) + authFactoryOutputs.append(impersonationEmailClient.getHeaders()["X-Appwrite-Impersonate-User-Email"] ?? "nil") + + let impersonationPhoneClient = try Client.fromImpersonation( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + session: "auth-session", + userPhone: "+15555550123" + ) + authFactoryOutputs.append(impersonationPhoneClient.getHeaders()["X-Appwrite-Impersonate-User-Phone"] ?? "nil") + + do { + _ = try Client.fromImpersonation( + endpoint: "https://cloud.appwrite.io/v1", + projectId: "auth-project", + session: "auth-session" + ) + } catch { + authFactoryOutputs.append(error.localizedDescription) + } + + do { + _ = try Client.fromBrowser( + endpoint: "htp://cloud.appwrite.io/v1", + projectId: "auth-project" + ) + } catch { + authFactoryOutputs.append(error.localizedDescription) + } + let client = Client() .setProject("123456") .addHeader(key: "Origin", value: "http://localhost") .setSelfSigned() let sdkHeaders = client.getHeaders() print("x-sdk-name: \(sdkHeaders["x-sdk-name"] ?? "nil"); x-sdk-platform: \(sdkHeaders["x-sdk-platform"] ?? "nil"); x-sdk-language: \(sdkHeaders["x-sdk-language"] ?? "nil"); x-sdk-version: \(sdkHeaders["x-sdk-version"] ?? "nil")") + authFactoryOutputs.forEach { print($0) } // Ping pong test let ping = try await client.ping() From 2c027e60e934dad2ec3fbdb768635b0acba95026 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 13:53:56 +0530 Subject: [PATCH 46/69] Tighten realtime auth factory surface --- templates/flutter/lib/src/client.dart.twig | 7 ++ .../flutter/lib/src/client_browser.dart.twig | 19 +++++ templates/flutter/lib/src/client_io.dart.twig | 68 ++++++++++++++++ .../lib/src/realtime_browser.dart.twig | 32 +------- .../flutter/lib/src/realtime_io.dart.twig | 80 +------------------ templates/node/src/client.ts.twig | 5 ++ tests/Base.php | 5 ++ tests/languages/node/test.js | 5 ++ 8 files changed, 112 insertions(+), 109 deletions(-) diff --git a/templates/flutter/lib/src/client.dart.twig b/templates/flutter/lib/src/client.dart.twig index 10a157b789..dcbaedc053 100644 --- a/templates/flutter/lib/src/client.dart.twig +++ b/templates/flutter/lib/src/client.dart.twig @@ -5,6 +5,7 @@ import 'client_stub.dart' import 'exception.dart'; import 'response.dart'; import 'upload_progress.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; /// [Client] that handles requests to {{spec.title | caseUcfirst}}. /// @@ -19,6 +20,12 @@ abstract class ClientAuth { /// {{spec.title | caseUcfirst}} realtime endpoint. String? get endPointRealtime; + /// Open a realtime WebSocket connection. + Future realtimeWebSocket(Uri uri); + + /// Session cookie fallback for browser realtime authentication. + String? realtimeFallbackCookie(); + /// Handle OAuth2 session creation. Future webAuth(Uri url, {String? callbackUrlScheme}); diff --git a/templates/flutter/lib/src/client_browser.dart.twig b/templates/flutter/lib/src/client_browser.dart.twig index 822057957c..89b6c890a9 100644 --- a/templates/flutter/lib/src/client_browser.dart.twig +++ b/templates/flutter/lib/src/client_browser.dart.twig @@ -1,9 +1,12 @@ +import 'dart:convert'; import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:http/http.dart' as http; import 'package:http/browser_client.dart'; import 'package:web/web.dart' as web; +import 'package:web_socket_channel/html.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; import 'client_mixin.dart'; import 'enums.dart'; import 'exception.dart'; @@ -122,6 +125,22 @@ class ClientBrowser extends ClientBase with ClientMixin { } } + @override + Future realtimeWebSocket(Uri uri) async { + await init(); + return HtmlWebSocketChannel.connect(uri); + } + + @override + String? realtimeFallbackCookie() { + final fallbackCookie = web.window.localStorage.getItem('cookieFallback'); + if (fallbackCookie != null) { + final cookie = Map.from(jsonDecode(fallbackCookie)); + return cookie.values.first; + } + return null; + } + @override Future chunkedUpload({ required String path, diff --git a/templates/flutter/lib/src/client_io.dart.twig b/templates/flutter/lib/src/client_io.dart.twig index e5fa2e7bae..4df85928ae 100644 --- a/templates/flutter/lib/src/client_io.dart.twig +++ b/templates/flutter/lib/src/client_io.dart.twig @@ -1,5 +1,6 @@ import 'dart:io'; import 'dart:math'; +import 'dart:convert'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:http/http.dart' as http; @@ -17,6 +18,8 @@ import 'response.dart'; import 'package:flutter/foundation.dart'; import 'input_file.dart'; import 'upload_progress.dart'; +import 'package:web_socket_channel/io.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; ClientBase createClient({required String endPoint, required bool selfSigned}) => ClientIO(endPoint: endPoint, selfSigned: selfSigned); @@ -197,6 +200,71 @@ class ClientIO extends ClientBase with ClientMixin { _initProgress = false; } + @override + Future realtimeWebSocket(Uri uri) async { + Map? headers; + while (!_initialized && _initProgress) { + await Future.delayed(Duration(milliseconds: 10)); + } + if (!_initialized) { + await init(); + } + final cookies = await _cookieJar.loadForRequest(uri); + headers = {HttpHeaders.cookieHeader: CookieManager.getCookies(cookies)}; + + return IOWebSocketChannel(selfSigned + ? await _connectRealtimeForSelfSignedCert(uri, headers) + : await WebSocket.connect(uri.toString(), headers: headers)); + } + + @override + String? realtimeFallbackCookie() => null; + + // https://github.com/jonataslaw/getsocket/blob/f25b3a264d8cc6f82458c949b86d286cd0343792/lib/src/io.dart#L104 + // and from official dart sdk websocket_impl.dart connect method + Future _connectRealtimeForSelfSignedCert( + Uri uri, Map headers) async { + try { + var r = Random(); + var key = base64.encode(List.generate(16, (_) => r.nextInt(255))); + var client = HttpClient(context: SecurityContext()); + client.badCertificateCallback = + (X509Certificate cert, String host, int port) { + return true; + }; + + uri = Uri( + scheme: uri.scheme == 'wss' ? 'https' : 'http', + userInfo: uri.userInfo, + host: uri.host, + port: uri.port, + path: uri.path, + query: uri.query, + fragment: uri.fragment, + ); + + var request = await client.getUrl(uri); + + headers.forEach((key, value) => request.headers.add(key, value)); + + request.headers + ..set(HttpHeaders.connectionHeader, "Upgrade") + ..set(HttpHeaders.upgradeHeader, "websocket") + ..set("Sec-WebSocket-Key", key) + ..set("Cache-Control", "no-cache") + ..set("Sec-WebSocket-Version", "13"); + + var response = await request.close(); + + // ignore: close_sinks + var socket = await response.detachSocket(); + var webSocket = WebSocket.fromUpgradedSocket(socket, serverSide: false); + return webSocket; + } catch (e) { + rethrow; + } + } + Future _interceptRequest(http.BaseRequest request) async { final body = (request is http.Request) ? request.body : ''; for (final i in _interceptors) { diff --git a/templates/flutter/lib/src/realtime_browser.dart.twig b/templates/flutter/lib/src/realtime_browser.dart.twig index 75417180bb..faab365b68 100644 --- a/templates/flutter/lib/src/realtime_browser.dart.twig +++ b/templates/flutter/lib/src/realtime_browser.dart.twig @@ -1,12 +1,6 @@ -import 'dart:convert'; -import 'dart:async'; -import 'package:web/web.dart' as web; -import 'package:web_socket_channel/html.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; import 'realtime_subscription.dart'; import 'realtime_base.dart'; import 'client.dart'; -import 'client_browser.dart'; import 'realtime_mixin.dart'; RealtimeBase createRealtime(ClientAuth client) => RealtimeBrowser(client); @@ -15,31 +9,9 @@ class RealtimeBrowser extends RealtimeBase with RealtimeMixin { Map? lastMessage; RealtimeBrowser(ClientAuth client) { - if (client is! ClientBrowser) { - throw ArgumentError.value( - client, - 'client', - 'RealtimeBrowser requires a ClientBrowser instance.', - ); - } - this.client = client; - getWebSocket = _getWebSocket; - getFallbackCookie = _getFallbackCookie; - } - - Future _getWebSocket(Uri uri) async { - await (client as ClientBrowser).init(); - return HtmlWebSocketChannel.connect(uri); - } - - String? _getFallbackCookie() { - final fallbackCookie = web.window.localStorage.getItem('cookieFallback'); - if (fallbackCookie != null) { - final cookie = Map.from(jsonDecode(fallbackCookie)); - return cookie.values.first; - } - return null; + getWebSocket = client.realtimeWebSocket; + getFallbackCookie = client.realtimeFallbackCookie; } @override diff --git a/templates/flutter/lib/src/realtime_io.dart.twig b/templates/flutter/lib/src/realtime_io.dart.twig index d5739906f3..55075f6be6 100644 --- a/templates/flutter/lib/src/realtime_io.dart.twig +++ b/templates/flutter/lib/src/realtime_io.dart.twig @@ -1,49 +1,15 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; -import 'package:flutter/foundation.dart'; -import 'package:web_socket_channel/io.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; -import 'cookie_manager.dart'; import 'realtime_subscription.dart'; import 'realtime_base.dart'; import 'realtime_mixin.dart'; import 'client.dart'; -import 'client_io.dart'; RealtimeBase createRealtime(ClientAuth client) => RealtimeIO(client); class RealtimeIO extends RealtimeBase with RealtimeMixin { RealtimeIO(ClientAuth client) { - if (client is! ClientIO) { - throw ArgumentError.value( - client, - 'client', - 'RealtimeIO requires a ClientIO instance.', - ); - } - this.client = client; - getWebSocket = _getWebSocket; - } - - Future _getWebSocket(Uri uri) async { - Map? headers; - while (!(client as ClientIO).initialized && (client as ClientIO).initProgress) { - await Future.delayed(Duration(milliseconds: 10)); - } - if (!(client as ClientIO).initialized) { - await (client as ClientIO).init(); - } - final cookies = await (client as ClientIO).cookieJar.loadForRequest(uri); - headers = {HttpHeaders.cookieHeader: CookieManager.getCookies(cookies)}; - - final websok = IOWebSocketChannel((client as ClientIO).selfSigned - ? await _connectForSelfSignedCert(uri, headers) - : await WebSocket.connect(uri.toString(), headers: headers)); - return websok; + getWebSocket = client.realtimeWebSocket; } /// Subscribe @@ -58,48 +24,4 @@ class RealtimeIO extends RealtimeBase with RealtimeMixin { return subscribeTo(channels, queries); } - // https://github.com/jonataslaw/getsocket/blob/f25b3a264d8cc6f82458c949b86d286cd0343792/lib/src/io.dart#L104 - // and from official dart sdk websocket_impl.dart connect method - Future _connectForSelfSignedCert( - Uri uri, Map headers) async { - try { - var r = Random(); - var key = base64.encode(List.generate(16, (_) => r.nextInt(255))); - var client = HttpClient(context: SecurityContext()); - client.badCertificateCallback = - (X509Certificate cert, String host, int port) { - return true; - }; - - uri = Uri( - scheme: uri.scheme == 'wss' ? 'https' : 'http', - userInfo: uri.userInfo, - host: uri.host, - port: uri.port, - path: uri.path, - query: uri.query, - fragment: uri.fragment, - ); - - var request = await client.getUrl(uri); - - headers.forEach((key, value) => request.headers.add(key, value)); - - request.headers - ..set(HttpHeaders.connectionHeader, "Upgrade") - ..set(HttpHeaders.upgradeHeader, "websocket") - ..set("Sec-WebSocket-Key", key) - ..set("Cache-Control", "no-cache") - ..set("Sec-WebSocket-Version", "13"); - - var response = await request.close(); - - // ignore: close_sinks - var socket = await response.detachSocket(); - var webSocket = WebSocket.fromUpgradedSocket(socket, serverSide: false); - return webSocket; - } catch (e) { - rethrow; - } - } } diff --git a/templates/node/src/client.ts.twig b/templates/node/src/client.ts.twig index 8df26c76a2..bca4842704 100644 --- a/templates/node/src/client.ts.twig +++ b/templates/node/src/client.ts.twig @@ -160,6 +160,7 @@ class Client { static fromCookie(params: BaseClientParams & { cookie: string }): Client { const client = new Client().applyBase(params, 'server'); + client.config.cookie = params.cookie; client.headers['Cookie'] = params.cookie; return client; } @@ -172,6 +173,7 @@ class Client { static fromDevKey(params: BaseClientParams & { devKey: string }): Client { const client = new Client().applyBase(params, 'client'); + client.config.devkey = params.devKey; client.headers['X-{{ spec.title | caseUcfirst }}-Dev-Key'] = params.devKey; return client; } @@ -192,13 +194,16 @@ class Client { .setSession(params.session); if (params.userId !== undefined) { + client.config.impersonateuserid = params.userId; client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id'] = params.userId; return client; } if (params.email !== undefined) { + client.config.impersonateuseremail = params.email; client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email'] = params.email; return client; } + client.config.impersonateuserphone = params.phone; client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone'] = params.phone; return client; } diff --git a/tests/Base.php b/tests/Base.php index 80022f0fe2..d77114a6d8 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -131,10 +131,15 @@ abstract class Base extends TestCase 'auth-session', 'auth-api-key', 'auth-cookie', + 'auth-cookie', 'auth-jwt', 'auth-dev-key', + 'auth-dev-key', + 'auth-user-id', 'auth-user-id', 'auth@example.com', + 'auth@example.com', + '+15555550123', '+15555550123', 'Exactly one impersonation target must be provided', 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', diff --git a/tests/languages/node/test.js b/tests/languages/node/test.js index 7c933412b6..4c9d3b2148 100644 --- a/tests/languages/node/test.js +++ b/tests/languages/node/test.js @@ -48,6 +48,7 @@ async function start() { cookie: 'auth-cookie' }); authFactoryOutputs.push(cookieClient.getHeaders()['Cookie']); + authFactoryOutputs.push(cookieClient.config.cookie); const jwtClient = Client.fromJWT({ endpoint: 'https://cloud.appwrite.io/v1', @@ -62,6 +63,7 @@ async function start() { devKey: 'auth-dev-key' }); authFactoryOutputs.push(devKeyClient.getHeaders()['X-Appwrite-Dev-Key']); + authFactoryOutputs.push(devKeyClient.config.devkey); const impersonationUserClient = Client.fromImpersonation({ endpoint: 'https://cloud.appwrite.io/v1', @@ -70,6 +72,7 @@ async function start() { userId: 'auth-user-id' }); authFactoryOutputs.push(impersonationUserClient.getHeaders()['X-Appwrite-Impersonate-User-Id']); + authFactoryOutputs.push(impersonationUserClient.config.impersonateuserid); const impersonationEmailClient = Client.fromImpersonation({ endpoint: 'https://cloud.appwrite.io/v1', @@ -78,6 +81,7 @@ async function start() { email: 'auth@example.com' }); authFactoryOutputs.push(impersonationEmailClient.getHeaders()['X-Appwrite-Impersonate-User-Email']); + authFactoryOutputs.push(impersonationEmailClient.config.impersonateuseremail); const impersonationPhoneClient = Client.fromImpersonation({ endpoint: 'https://cloud.appwrite.io/v1', @@ -86,6 +90,7 @@ async function start() { phone: '+15555550123' }); authFactoryOutputs.push(impersonationPhoneClient.getHeaders()['X-Appwrite-Impersonate-User-Phone']); + authFactoryOutputs.push(impersonationPhoneClient.config.impersonateuserphone); try { Client.fromImpersonation({ From a7ad1d91eb2ef82c9ea7b6f2ae0a00e255bc5c8a Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 14:05:27 +0530 Subject: [PATCH 47/69] Use SDK exception for Flutter impersonation validation --- templates/flutter/lib/src/client.dart.twig | 2 +- tests/languages/flutter/tests.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/flutter/lib/src/client.dart.twig b/templates/flutter/lib/src/client.dart.twig index dcbaedc053..eede24e95c 100644 --- a/templates/flutter/lib/src/client.dart.twig +++ b/templates/flutter/lib/src/client.dart.twig @@ -202,7 +202,7 @@ abstract class Client implements ClientAuth { }) { final targets = [userId, userEmail, userPhone].whereType().length; if (targets != 1) { - throw ArgumentError( + throw {{spec.title | caseUcfirst}}Exception( 'Exactly one of userId, userEmail, or userPhone must be provided.', ); } diff --git a/tests/languages/flutter/tests.dart b/tests/languages/flutter/tests.dart index 916e428d84..c9604a6b51 100644 --- a/tests/languages/flutter/tests.dart +++ b/tests/languages/flutter/tests.dart @@ -110,7 +110,7 @@ void main() async { projectId: 'auth-project', session: 'auth-session', ); - } on ArgumentError catch (e) { + } on AppwriteException catch (e) { authFactoryOutputs.add(e.message); } From d44a37014d9c3cd2bf3501e29fe110b61fdc31fd Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 14:33:24 +0530 Subject: [PATCH 48/69] Standardize auth factory test expectations --- .../src/main/java/io/package/Client.kt.twig | 2 +- templates/apple/Sources/Client.swift.twig | 2 +- templates/dart/lib/src/client.dart.twig | 2 +- templates/flutter/lib/src/client.dart.twig | 2 +- .../main/kotlin/io/appwrite/Client.kt.twig | 2 +- templates/swift/Sources/Client.swift.twig | 2 +- tests/Android14Java11Test.php | 2 +- tests/Android14Java17Test.php | 2 +- tests/Android14Java8Test.php | 2 +- tests/Android5Java17Test.php | 2 +- tests/AppleSwift56Test.php | 2 +- tests/Base.php | 89 +------------------ tests/DartBetaTest.php | 2 +- tests/DartStableTest.php | 2 +- tests/FlutterBetaTest.php | 2 +- tests/FlutterStableTest.php | 2 +- tests/KotlinJava11Test.php | 2 +- tests/KotlinJava17Test.php | 2 +- tests/KotlinJava8Test.php | 2 +- tests/Node16Test.php | 2 +- tests/Node18Test.php | 2 +- tests/Node20Test.php | 2 +- tests/Swift56Test.php | 2 +- tests/WebChromiumTest.php | 2 +- tests/WebNodeTest.php | 2 +- tests/languages/node/test.js | 5 -- 26 files changed, 28 insertions(+), 114 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Client.kt.twig b/templates/android/library/src/main/java/io/package/Client.kt.twig index 31f0e48b9b..4569788db8 100644 --- a/templates/android/library/src/main/java/io/package/Client.kt.twig +++ b/templates/android/library/src/main/java/io/package/Client.kt.twig @@ -267,7 +267,7 @@ class Client @JvmOverloads constructor( ): ClientAuth { val targets = listOfNotNull(userId, userEmail, userPhone).size require(targets == 1) { - "Exactly one of userId, userEmail, or userPhone must be provided." + "Exactly one impersonation target must be provided" } val client = fromBrowserClient( diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index 59385cb369..88315a7817 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -204,7 +204,7 @@ open class Client: ClientAuth { ) throws -> ClientAuth { let targetCount = [userId, userEmail, userPhone].compactMap { $0 }.count guard targetCount == 1 else { - throw {{ spec.title | caseUcfirst }}Error(message: "Provide exactly one impersonation target.") + throw {{ spec.title | caseUcfirst }}Error(message: "Exactly one impersonation target must be provided") } let client = Client() diff --git a/templates/dart/lib/src/client.dart.twig b/templates/dart/lib/src/client.dart.twig index 7627482db5..4c04f70796 100644 --- a/templates/dart/lib/src/client.dart.twig +++ b/templates/dart/lib/src/client.dart.twig @@ -148,7 +148,7 @@ abstract class Client { final targets = [userId, userEmail, userPhone].whereType().length; if (targets != 1) { throw {{spec.title | caseUcfirst}}Exception( - 'Exactly one of userId, userEmail, or userPhone must be provided.', + 'Exactly one impersonation target must be provided', ); } diff --git a/templates/flutter/lib/src/client.dart.twig b/templates/flutter/lib/src/client.dart.twig index eede24e95c..ac4c4ccf4d 100644 --- a/templates/flutter/lib/src/client.dart.twig +++ b/templates/flutter/lib/src/client.dart.twig @@ -203,7 +203,7 @@ abstract class Client implements ClientAuth { final targets = [userId, userEmail, userPhone].whereType().length; if (targets != 1) { throw {{spec.title | caseUcfirst}}Exception( - 'Exactly one of userId, userEmail, or userPhone must be provided.', + 'Exactly one impersonation target must be provided', ); } diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig index f4b6a96d80..b06ee5b41a 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig @@ -163,7 +163,7 @@ class Client @JvmOverloads constructor( ): Client { val targets = listOfNotNull(userId, userEmail, userPhone).size require(targets == 1) { - "Exactly one of userId, userEmail, or userPhone must be provided." + "Exactly one impersonation target must be provided" } val client = fromBrowser( diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index 83857afac0..f8aa4352a1 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -141,7 +141,7 @@ open class Client { ) throws -> Client { let targetCount = [userId, userEmail, userPhone].compactMap { $0 }.count guard targetCount == 1 else { - throw {{ spec.title | caseUcfirst }}Error(message: "Provide exactly one impersonation target.") + throw {{ spec.title | caseUcfirst }}Error(message: "Exactly one impersonation target must be provided") } let client = try fromBrowser(endpoint: endpoint, projectId: projectId, locale: locale, selfSigned: selfSigned) diff --git a/tests/Android14Java11Test.php b/tests/Android14Java11Test.php index bfc2f25b0c..b57dfdfab9 100644 --- a/tests/Android14Java11Test.php +++ b/tests/Android14Java11Test.php @@ -20,7 +20,7 @@ class Android14Java11Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-34-jdk11 sh -c "./gradlew :library:testReleaseUnitTest --stacktrace -q && cat library/result.txt"'; protected array $expectedOutput = [ - ...Base::ANDROID_AUTH_FACTORY_RESPONSES, + ...Base::CLIENT_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/Android14Java17Test.php b/tests/Android14Java17Test.php index 9b71782a5d..b86ae27fd1 100644 --- a/tests/Android14Java17Test.php +++ b/tests/Android14Java17Test.php @@ -20,7 +20,7 @@ class Android14Java17Test extends Base 'docker run --rm --network="mockapi" -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-34-jdk17 sh -c "./gradlew :library:testReleaseUnitTest --stacktrace -q && cat library/result.txt"'; protected array $expectedOutput = [ - ...Base::ANDROID_AUTH_FACTORY_RESPONSES, + ...Base::CLIENT_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/Android14Java8Test.php b/tests/Android14Java8Test.php index c0a85568ed..6f2847945c 100644 --- a/tests/Android14Java8Test.php +++ b/tests/Android14Java8Test.php @@ -20,7 +20,7 @@ class Android14Java8Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-34-jdk8 sh -c "./gradlew :library:testReleaseUnitTest --stacktrace -q && cat library/result.txt"'; protected array $expectedOutput = [ - ...Base::ANDROID_AUTH_FACTORY_RESPONSES, + ...Base::CLIENT_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/Android5Java17Test.php b/tests/Android5Java17Test.php index 3525793641..a04ef1243c 100644 --- a/tests/Android5Java17Test.php +++ b/tests/Android5Java17Test.php @@ -20,7 +20,7 @@ class Android5Java17Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-21-jdk17 sh -c "./gradlew :library:testReleaseUnitTest --stacktrace -q && cat library/result.txt"'; protected array $expectedOutput = [ - ...Base::ANDROID_AUTH_FACTORY_RESPONSES, + ...Base::CLIENT_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/AppleSwift56Test.php b/tests/AppleSwift56Test.php index a7af09612f..00530b59b3 100644 --- a/tests/AppleSwift56Test.php +++ b/tests/AppleSwift56Test.php @@ -19,7 +19,7 @@ class AppleSwift56Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/apple swift:5.6-focal swift test'; protected array $expectedOutput = [ - ...Base::APPLE_AUTH_FACTORY_RESPONSES, + ...Base::CLIENT_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/Base.php b/tests/Base.php index d77114a6d8..617dd977dc 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -109,10 +109,9 @@ abstract class Base extends TestCase 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', ]; - protected const WEB_AUTH_FACTORY_RESPONSES = [ + protected const CLIENT_AUTH_FACTORY_RESPONSES = [ 'auth-project', 'en-US', - 'client', 'wss://realtime.example.com/v1', 'auth-session', 'auth-dev-key', @@ -124,113 +123,33 @@ abstract class Base extends TestCase 'Invalid realtime endpoint URL: ftp://cloud.appwrite.io/v1', ]; - protected const NODE_AUTH_FACTORY_RESPONSES = [ + protected const SERVER_AUTH_FACTORY_RESPONSES = [ 'auth-project', 'en-US', 'client', 'auth-session', 'auth-api-key', 'auth-cookie', - 'auth-cookie', 'auth-jwt', 'auth-dev-key', - 'auth-dev-key', 'auth-user-id', - 'auth-user-id', - 'auth@example.com', 'auth@example.com', '+15555550123', - '+15555550123', 'Exactly one impersonation target must be provided', 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', ]; - protected const DART_AUTH_FACTORY_RESPONSES = [ - 'auth-project', - 'en-US', - 'client', - 'auth-session', - 'auth-api-key', - 'auth-cookie', - 'auth-jwt', - 'auth-dev-key', - 'auth-user-id', - 'auth@example.com', - '+15555550123', - 'Exactly one of userId, userEmail, or userPhone must be provided.', - 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', - ]; - - protected const SWIFT_AUTH_FACTORY_RESPONSES = [ - 'auth-project', - 'en-US', - 'client', - 'auth-session', - 'auth-api-key', - 'auth-cookie', - 'auth-jwt', - 'auth-dev-key', - 'auth-user-id', - 'auth@example.com', - '+15555550123', - 'Provide exactly one impersonation target.', - 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', - ]; - - protected const KOTLIN_AUTH_FACTORY_RESPONSES = [ + protected const ISOMORPHIC_AUTH_FACTORY_RESPONSES = [ 'auth-project', 'en-US', 'client', - 'auth-session', - 'auth-api-key', - 'auth-cookie', - 'auth-jwt', - 'auth-dev-key', - 'auth-user-id', - 'auth@example.com', - '+15555550123', - 'Exactly one of userId, userEmail, or userPhone must be provided.', - 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', - ]; - - protected const APPLE_AUTH_FACTORY_RESPONSES = [ - 'auth-project', - 'en-US', - 'wss://realtime.example.com/v1', - 'auth-session', - 'auth-dev-key', - 'auth-user-id', - 'auth@example.com', - '+15555550123', - 'Provide exactly one impersonation target.', - 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', - 'Invalid realtime endpoint URL: ftp://cloud.appwrite.io/v1', - ]; - - protected const ANDROID_AUTH_FACTORY_RESPONSES = [ - 'auth-project', - 'en-US', - 'wss://realtime.example.com/v1', - 'auth-session', - 'auth-dev-key', - 'auth-user-id', - 'auth@example.com', - '+15555550123', - 'Exactly one of userId, userEmail, or userPhone must be provided.', - 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', - 'Invalid realtime endpoint URL: ftp://cloud.appwrite.io/v1', - ]; - - protected const FLUTTER_AUTH_FACTORY_RESPONSES = [ - 'auth-project', - 'en-US', 'wss://realtime.example.com/v1', 'auth-session', 'auth-dev-key', 'auth-user-id', 'auth@example.com', '+15555550123', - 'Exactly one of userId, userEmail, or userPhone must be provided.', + 'Exactly one impersonation target must be provided', 'Invalid endpoint URL: htp://cloud.appwrite.io/v1', 'Invalid realtime endpoint URL: ftp://cloud.appwrite.io/v1', ]; diff --git a/tests/DartBetaTest.php b/tests/DartBetaTest.php index 4f83344f83..4bed463b5d 100644 --- a/tests/DartBetaTest.php +++ b/tests/DartBetaTest.php @@ -19,7 +19,7 @@ class DartBetaTest extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/dart dart:beta sh -c "dart pub get && dart pub run tests/tests.dart"'; protected array $expectedOutput = [ - ...Base::DART_AUTH_FACTORY_RESPONSES, + ...Base::SERVER_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/DartStableTest.php b/tests/DartStableTest.php index 6b93f8dac0..76c9b55a3f 100644 --- a/tests/DartStableTest.php +++ b/tests/DartStableTest.php @@ -19,7 +19,7 @@ class DartStableTest extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/dart dart:stable sh -c "dart pub get && dart pub run tests/tests.dart"'; protected array $expectedOutput = [ - ...Base::DART_AUTH_FACTORY_RESPONSES, + ...Base::SERVER_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/FlutterBetaTest.php b/tests/FlutterBetaTest.php index a40271a6f7..4ca0194896 100644 --- a/tests/FlutterBetaTest.php +++ b/tests/FlutterBetaTest.php @@ -19,7 +19,7 @@ class FlutterBetaTest extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/flutter ghcr.io/cirruslabs/flutter:beta sh -c "flutter pub get && flutter test test/appwrite_test.dart"'; protected array $expectedOutput = [ - ...Base::FLUTTER_AUTH_FACTORY_RESPONSES, + ...Base::CLIENT_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/FlutterStableTest.php b/tests/FlutterStableTest.php index 8b0109afc4..2ef6c77c9f 100644 --- a/tests/FlutterStableTest.php +++ b/tests/FlutterStableTest.php @@ -19,7 +19,7 @@ class FlutterStableTest extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app:rw -w /app/tests/sdks/flutter ghcr.io/cirruslabs/flutter:stable sh -c "flutter pub get && flutter test test/appwrite_test.dart"'; protected array $expectedOutput = [ - ...Base::FLUTTER_AUTH_FACTORY_RESPONSES, + ...Base::CLIENT_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/KotlinJava11Test.php b/tests/KotlinJava11Test.php index 5c674b4707..cc8b94efec 100644 --- a/tests/KotlinJava11Test.php +++ b/tests/KotlinJava11Test.php @@ -20,7 +20,7 @@ class KotlinJava11Test extends Base 'docker run --network="mockapi" -v $(pwd):/app -w /app/tests/sdks/kotlin eclipse-temurin:11-jdk-jammy sh -c "./gradlew test -q && cat result.txt"'; protected array $expectedOutput = [ - ...Base::KOTLIN_AUTH_FACTORY_RESPONSES, + ...Base::SERVER_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/KotlinJava17Test.php b/tests/KotlinJava17Test.php index d63cb118aa..ad618a23e0 100644 --- a/tests/KotlinJava17Test.php +++ b/tests/KotlinJava17Test.php @@ -20,7 +20,7 @@ class KotlinJava17Test extends Base 'docker run --network="mockapi" -v $(pwd):/app -w /app/tests/sdks/kotlin eclipse-temurin:17-jdk-jammy sh -c "./gradlew test -q && cat result.txt"'; protected array $expectedOutput = [ - ...Base::KOTLIN_AUTH_FACTORY_RESPONSES, + ...Base::SERVER_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/KotlinJava8Test.php b/tests/KotlinJava8Test.php index aa7901b86a..f1ad0988ec 100644 --- a/tests/KotlinJava8Test.php +++ b/tests/KotlinJava8Test.php @@ -20,7 +20,7 @@ class KotlinJava8Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/kotlin eclipse-temurin:8-jdk-jammy sh -c "./gradlew test -q && cat result.txt"'; protected array $expectedOutput = [ - ...Base::KOTLIN_AUTH_FACTORY_RESPONSES, + ...Base::SERVER_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/Node16Test.php b/tests/Node16Test.php index 7f8af1d792..829ce61947 100644 --- a/tests/Node16Test.php +++ b/tests/Node16Test.php @@ -20,7 +20,7 @@ class Node16Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app node:16-alpine node tests/sdks/node/test.js'; protected array $expectedOutput = [ - ...Base::NODE_AUTH_FACTORY_RESPONSES, + ...Base::SERVER_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::FOO_RESPONSES, // Object params diff --git a/tests/Node18Test.php b/tests/Node18Test.php index 7ef04e6427..4f7b8e5539 100644 --- a/tests/Node18Test.php +++ b/tests/Node18Test.php @@ -20,7 +20,7 @@ class Node18Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app node:18-alpine node tests/sdks/node/test.js'; protected array $expectedOutput = [ - ...Base::NODE_AUTH_FACTORY_RESPONSES, + ...Base::SERVER_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::FOO_RESPONSES, // Object params diff --git a/tests/Node20Test.php b/tests/Node20Test.php index 0cc4356ab1..e9efdbf373 100644 --- a/tests/Node20Test.php +++ b/tests/Node20Test.php @@ -20,7 +20,7 @@ class Node20Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app node:20-alpine node tests/sdks/node/test.js'; protected array $expectedOutput = [ - ...Base::NODE_AUTH_FACTORY_RESPONSES, + ...Base::SERVER_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::FOO_RESPONSES, // Object params diff --git a/tests/Swift56Test.php b/tests/Swift56Test.php index 8d3a731eac..d672040bed 100644 --- a/tests/Swift56Test.php +++ b/tests/Swift56Test.php @@ -19,7 +19,7 @@ class Swift56Test extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/swift swift:5.6-focal swift test'; protected array $expectedOutput = [ - ...Base::SWIFT_AUTH_FACTORY_RESPONSES, + ...Base::SERVER_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/WebChromiumTest.php b/tests/WebChromiumTest.php index 9f98ad9d10..2e167471ac 100644 --- a/tests/WebChromiumTest.php +++ b/tests/WebChromiumTest.php @@ -21,7 +21,7 @@ class WebChromiumTest extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -e BROWSER=chromium -w /app/tests/sdks/web mcr.microsoft.com/playwright:v1.56.1-jammy node tests.js'; protected array $expectedOutput = [ - ...Base::WEB_AUTH_FACTORY_RESPONSES, + ...Base::ISOMORPHIC_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::FOO_RESPONSES, // Object params diff --git a/tests/WebNodeTest.php b/tests/WebNodeTest.php index 181b6c242a..4e7b83cba4 100644 --- a/tests/WebNodeTest.php +++ b/tests/WebNodeTest.php @@ -22,7 +22,7 @@ class WebNodeTest extends Base 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/web node:18-alpine node node.js'; protected array $expectedOutput = [ - ...Base::WEB_AUTH_FACTORY_RESPONSES, + ...Base::ISOMORPHIC_AUTH_FACTORY_RESPONSES, ...Base::PING_RESPONSE, ...Base::FOO_RESPONSES, ...Base::FOO_RESPONSES, // Object params diff --git a/tests/languages/node/test.js b/tests/languages/node/test.js index 4c9d3b2148..7c933412b6 100644 --- a/tests/languages/node/test.js +++ b/tests/languages/node/test.js @@ -48,7 +48,6 @@ async function start() { cookie: 'auth-cookie' }); authFactoryOutputs.push(cookieClient.getHeaders()['Cookie']); - authFactoryOutputs.push(cookieClient.config.cookie); const jwtClient = Client.fromJWT({ endpoint: 'https://cloud.appwrite.io/v1', @@ -63,7 +62,6 @@ async function start() { devKey: 'auth-dev-key' }); authFactoryOutputs.push(devKeyClient.getHeaders()['X-Appwrite-Dev-Key']); - authFactoryOutputs.push(devKeyClient.config.devkey); const impersonationUserClient = Client.fromImpersonation({ endpoint: 'https://cloud.appwrite.io/v1', @@ -72,7 +70,6 @@ async function start() { userId: 'auth-user-id' }); authFactoryOutputs.push(impersonationUserClient.getHeaders()['X-Appwrite-Impersonate-User-Id']); - authFactoryOutputs.push(impersonationUserClient.config.impersonateuserid); const impersonationEmailClient = Client.fromImpersonation({ endpoint: 'https://cloud.appwrite.io/v1', @@ -81,7 +78,6 @@ async function start() { email: 'auth@example.com' }); authFactoryOutputs.push(impersonationEmailClient.getHeaders()['X-Appwrite-Impersonate-User-Email']); - authFactoryOutputs.push(impersonationEmailClient.config.impersonateuseremail); const impersonationPhoneClient = Client.fromImpersonation({ endpoint: 'https://cloud.appwrite.io/v1', @@ -90,7 +86,6 @@ async function start() { phone: '+15555550123' }); authFactoryOutputs.push(impersonationPhoneClient.getHeaders()['X-Appwrite-Impersonate-User-Phone']); - authFactoryOutputs.push(impersonationPhoneClient.config.impersonateuserphone); try { Client.fromImpersonation({ From 734572ae24f19c86fda2e45123231d65d1952078 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 14:53:54 +0530 Subject: [PATCH 49/69] Expand isomorphic auth factory test coverage --- templates/web/src/client.ts.twig | 10 ---------- tests/Base.php | 3 +++ tests/languages/web/index.html | 21 +++++++++++++++++++++ tests/languages/web/node.js | 21 +++++++++++++++++++++ 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index ac887240ef..76133b3512 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -377,13 +377,9 @@ type ClientConstructor = { new (): LegacyClient; fromBrowser(params: Prettify): Client<'browser'>; fromSession(params: Prettify): Client<'session'>; -{%~ if sdk.platform == 'server' or sdk.platform == 'console' %} fromAPIKey(params: Prettify): Client<'apiKey'>; -{%~ endif %} fromCookie(params: Prettify): Client<'cookie'>; -{%~ if sdk.platform == 'server' or sdk.platform == 'console' %} fromJWT(params: Prettify): Client<'jwt'>; -{%~ endif %} fromDevKey(params: Prettify): Client<'devKey'>; fromImpersonation(params: Prettify): Client<'impersonation'>; }; @@ -436,15 +432,12 @@ class ClientRuntime { .setSession(params.session); } -{%~ if sdk.platform == 'server' or sdk.platform == 'console' %} static fromAPIKey(params: Prettify): Client<'apiKey'> { return new ClientRuntime<'apiKey'>() .applyBase<'apiKey'>(params, 'server') .setKey(params.apiKey); } -{%~ endif %} - static fromCookie(params: Prettify): Client<'cookie'> { return new ClientRuntime<'cookie'>() {%~ if sdk.platform == 'console' %} @@ -455,15 +448,12 @@ class ClientRuntime { .setCookie(params.cookie); } -{%~ if sdk.platform == 'server' or sdk.platform == 'console' %} static fromJWT(params: Prettify): Client<'jwt'> { return new ClientRuntime<'jwt'>() .applyBase<'jwt'>(params, 'server') .setJWT(params.jwt); } -{%~ endif %} - static fromDevKey(params: Prettify): Client<'devKey'> { return new ClientRuntime<'devKey'>() .applyBase<'devKey'>(params, 'client') diff --git a/tests/Base.php b/tests/Base.php index 617dd977dc..84ec1e36cc 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -145,6 +145,9 @@ abstract class Base extends TestCase 'client', 'wss://realtime.example.com/v1', 'auth-session', + 'auth-api-key', + 'auth-cookie', + 'auth-jwt', 'auth-dev-key', 'auth-user-id', 'auth@example.com', diff --git a/tests/languages/web/index.html b/tests/languages/web/index.html index 7122bd5496..f119ca7a68 100644 --- a/tests/languages/web/index.html +++ b/tests/languages/web/index.html @@ -60,6 +60,27 @@ }); console.log(sessionClient.getHeaders()['X-Appwrite-Session']); + const apiKeyClient = Client.fromAPIKey({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + apiKey: 'auth-api-key', + }); + console.log(apiKeyClient.getHeaders()['X-Appwrite-Key']); + + const cookieClient = Client.fromCookie({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + cookie: 'auth-cookie', + }); + console.log(cookieClient.getHeaders()['Cookie']); + + const jwtClient = Client.fromJWT({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + jwt: 'auth-jwt', + }); + console.log(jwtClient.getHeaders()['X-Appwrite-JWT']); + const devKeyClient = Client.fromDevKey({ endpoint: 'https://cloud.appwrite.io/v1', projectId: 'auth-project', diff --git a/tests/languages/web/node.js b/tests/languages/web/node.js index d93733c091..a80c5e3a63 100644 --- a/tests/languages/web/node.js +++ b/tests/languages/web/node.js @@ -34,6 +34,27 @@ async function start() { }); console.log(sessionClient.getHeaders()['X-Appwrite-Session']); + const apiKeyClient = Client.fromAPIKey({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + apiKey: 'auth-api-key', + }); + console.log(apiKeyClient.getHeaders()['X-Appwrite-Key']); + + const cookieClient = Client.fromCookie({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + cookie: 'auth-cookie', + }); + console.log(cookieClient.getHeaders()['Cookie']); + + const jwtClient = Client.fromJWT({ + endpoint: 'https://cloud.appwrite.io/v1', + projectId: 'auth-project', + jwt: 'auth-jwt', + }); + console.log(jwtClient.getHeaders()['X-Appwrite-JWT']); + const devKeyClient = Client.fromDevKey({ endpoint: 'https://cloud.appwrite.io/v1', projectId: 'auth-project', From 2bb7ce489a722904333f8eb9797f556ac00721a6 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 14:57:24 +0530 Subject: [PATCH 50/69] Fix web client API key factory --- templates/web/src/client.ts.twig | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 76133b3512..f0aff0a7a5 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -433,9 +433,10 @@ class ClientRuntime { } static fromAPIKey(params: Prettify): Client<'apiKey'> { - return new ClientRuntime<'apiKey'>() - .applyBase<'apiKey'>(params, 'server') - .setKey(params.apiKey); + const client = new ClientRuntime<'apiKey'>() + .applyBase<'apiKey'>(params, 'server'); + client.headers['X-{{ spec.title | caseUcfirst }}-Key'] = params.apiKey; + return client; } static fromCookie(params: Prettify): Client<'cookie'> { From a541ccd51b5ca9ce5e5e21ca2928afbc8ddcc122 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 15:01:28 +0530 Subject: [PATCH 51/69] Rename web auth receiver filter --- src/SDK/Language/Web.php | 8 ++++---- templates/web/src/services/template.ts.twig | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index de2769f330..8d8c768b2c 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -547,9 +547,9 @@ public function webServiceAuth(array $service): array } /** - * Build the TypeScript `this:` gate string for a method in a Web service. + * Build the TypeScript `this:` receiver constraint for a method in a Web service. */ - public function webMethodThisGate(array $method, array $service): string + public function webMethodAuthReceiver(array $method, array $service): string { $auth = $this->webServiceAuth($service); if (!$auth['hasMixedTier']) { @@ -680,8 +680,8 @@ public function getFilters(): array new TwigFilter('webServiceAuth', function (array $service) { return $this->webServiceAuth($service); }), - new TwigFilter('webMethodThisGate', function (array $method, array $service) { - return $this->webMethodThisGate($method, $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); diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index 01059957d5..958c56a01a 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -54,7 +54,7 @@ class {{ service.name | caseUcfirst }}Runtime { {%~ endif %} {%~ for method in service.methods %} - {%~ set thisGate = method | webMethodThisGate(service) %} + {%~ set authReceiver = method | webMethodAuthReceiver(service) %} /** {%~ if method.description %} * {{ method.description | replace({'\n': '\n * '}) | raw }} @@ -74,7 +74,7 @@ class {{ service.name | caseUcfirst }}Runtime { {%~ endif %} */ {%~ if method.parameters.all|length > 0 %} - {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({{ thisGate | raw }}params{% set hasRequiredParams = false %}{% for parameter in method.parameters.all %}{% if parameter.required %}{% set hasRequiredParams = true %}{% endif %}{% endfor %}{% if not hasRequiredParams %}?{% endif %}: { {% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress?: (progress: UploadProgress) => void{% endif %} }): {{ method | getReturn(spec) | raw }}; + {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({{ authReceiver | raw }}params{% set hasRequiredParams = false %}{% for parameter in method.parameters.all %}{% if parameter.required %}{% set hasRequiredParams = true %}{% endif %}{% endfor %}{% if not hasRequiredParams %}?{% endif %}: { {% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress?: (progress: UploadProgress) => void{% endif %} }): {{ method | getReturn(spec) | raw }}; /** {%~ if method.description %} * {{ method.description | replace({'\n': '\n * '}) | raw }} @@ -87,7 +87,7 @@ class {{ service.name | caseUcfirst }}Runtime { * @returns {{ '{' }}{{ method | getReturn(spec) | raw }}{{ '}' }} * @deprecated Use the object parameter style method for a better developer experience. */ - {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({{ thisGate | raw }}{% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress?: (progress: UploadProgress) => void{% endif %}): {{ method | getReturn(spec) | raw }}; + {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({{ authReceiver | raw }}{% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress?: (progress: UploadProgress) => void{% endif %}): {{ method | getReturn(spec) | raw }}; {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}( {% if method.parameters.all|length > 0 %}paramsOrFirst{% if not method.parameters.all[0].required or method.parameters.all[0].nullable %}?{% endif %}: { {% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress?: (progress: UploadProgress) => void{% endif %} } | {{ method.parameters.all[0] | getPropertyType(method) | raw }}{% if method.parameters.all|length > 1 %}, ...rest: [{% for parameter in method.parameters.all[1:] %}({{ parameter | getPropertyType(method) | raw }})?{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %},((progress: UploadProgress) => void)?{% endif %}]{% endif %}{% endif %} @@ -129,8 +129,8 @@ class {{ service.name | caseUcfirst }}Runtime { {%~ endif %} {%~ endif %} {%~ else %} - {%~ if thisGate is not empty %} - {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({{ thisGate | raw }}{% if 'multipart/form-data' in method.consumes %}onProgress?: (progress: UploadProgress) => void{% endif %}): {{ method | getReturn(spec) | raw }}; + {%~ if authReceiver is not empty %} + {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({{ authReceiver | raw }}{% if 'multipart/form-data' in method.consumes %}onProgress?: (progress: UploadProgress) => void{% endif %}): {{ method | getReturn(spec) | raw }}; {%~ endif %} {{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress = (progress: UploadProgress) => void{% endif %}): {{ method | getReturn(spec) | raw }} { {%~ endif %} From dce0f0e3b7ecfa88640570df6072ccfca267ba7f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 17:30:14 +0530 Subject: [PATCH 52/69] Fix isomorphic auth template issues --- src/SDK/Language/Web.php | 23 +++++++ .../src/main/java/io/package/Client.kt.twig | 10 ++- .../Sources/Services/Realtime.swift.twig | 12 +++- templates/web/src/client.ts.twig | 61 ++++++++++++------- templates/web/src/services/template.ts.twig | 2 + 5 files changed, 83 insertions(+), 25 deletions(-) diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index 8d8c768b2c..17efc8f0f7 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -505,6 +505,7 @@ public function webServiceAuth(array $service): array $hasServerOnly = false; $hasClientOnly = false; $hasUpload = false; + $needsServiceFlatten = false; $serverOnlyMethods = []; $clientOnlyMethods = []; @@ -530,6 +531,9 @@ public function webServiceAuth(array $service): array if (in_array('multipart/form-data', $method['consumes'] ?? [], true)) { $hasUpload = true; } + if (in_array($method['type'] ?? '', ['location', 'webAuth'], true)) { + $needsServiceFlatten = true; + } } $hasMixedTier = $hasClientTier && $hasServerTier; @@ -541,11 +545,27 @@ public function webServiceAuth(array $service): array '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. */ @@ -686,6 +706,9 @@ public function getFilters(): array 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']]), diff --git a/templates/android/library/src/main/java/io/package/Client.kt.twig b/templates/android/library/src/main/java/io/package/Client.kt.twig index 4569788db8..63ad4b240a 100644 --- a/templates/android/library/src/main/java/io/package/Client.kt.twig +++ b/templates/android/library/src/main/java/io/package/Client.kt.twig @@ -161,7 +161,9 @@ class Client @JvmOverloads constructor( val client = Client( context, endpoint, - endpointRealtime ?: endpoint.replaceFirst("http", "ws"), + endpointRealtime ?: endpoint + .replaceFirst("https://", "wss://") + .replaceFirst("http://", "ws://"), selfSigned ) @@ -190,6 +192,9 @@ class Client @JvmOverloads constructor( value: String ) { client.config[configKey] = value + if (configKey != configKey.lowercase()) { + client.config[configKey.lowercase()] = value + } client.addHeader(header, value) } @@ -372,6 +377,9 @@ class Client @JvmOverloads constructor( @Deprecated("Use Client.fromBrowser or another factory method instead.") fun set{{header.key | caseUcfirst}}(value: String): Client { config["{{ header.key | caseCamel }}"] = value +{%~ if (header.key | caseCamel) != (header.key | caseLower) %} + config["{{ header.key | caseLower }}"] = value +{%~ endif %} addHeader("{{ header.name | caseLower }}", value) return this } diff --git a/templates/apple/Sources/Services/Realtime.swift.twig b/templates/apple/Sources/Services/Realtime.swift.twig index 9f1636d44f..5f37778874 100644 --- a/templates/apple/Sources/Services/Realtime.swift.twig +++ b/templates/apple/Sources/Services/Realtime.swift.twig @@ -83,9 +83,17 @@ open class Realtime : Service { return } - let queryParams = "project=\(client.getConfig(key: "project")!)" + guard let project = client.getConfig(key: "project") else { + throw {{ spec.title | caseUcfirst }}Error(message: "Missing project") + } + + guard let endPointRealtime = client.endPointRealtime else { + throw {{ spec.title | caseUcfirst }}Error(message: "Missing realtime endpoint") + } + + let queryParams = "project=\(project)" - let url = "\(client.endPointRealtime!)/realtime?\(queryParams)" + let url = "\(endPointRealtime)/realtime?\(queryParams)" if (socketClient != nil) { reconnect = false diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index f0aff0a7a5..8c7635d6fb 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -394,15 +394,15 @@ class ClientRuntime { config: { endpoint: string; endpointRealtime: string; -{%~ for header in spec.global.headers %} - {{ header.key | caseLower }}: string; +{%~ for key in spec.global.headers | webClientConfigKeys %} + {{ key }}: string; {%~ endfor %} selfSigned: boolean; } = { endpoint: '{{ spec.endpoint }}', endpointRealtime: '', -{%~ for header in spec.global.headers %} - {{ header.key | caseLower }}: '', +{%~ for key in spec.global.headers | webClientConfigKeys %} + {{ key }}: '', {%~ endfor %} selfSigned: false, }; @@ -427,38 +427,47 @@ class ClientRuntime { } static fromSession(params: Prettify): Client<'session'> { - return new ClientRuntime<'session'>() - .applyBase<'session'>(params, 'client') - .setSession(params.session); + const client = new ClientRuntime<'session'>() + .applyBase<'session'>(params, 'client'); + client.headers['X-{{ spec.title | caseUcfirst }}-Session'] = params.session; + client.config.session = params.session; + return client; } static fromAPIKey(params: Prettify): Client<'apiKey'> { const client = new ClientRuntime<'apiKey'>() .applyBase<'apiKey'>(params, 'server'); client.headers['X-{{ spec.title | caseUcfirst }}-Key'] = params.apiKey; + client.config.key = params.apiKey; return client; } static fromCookie(params: Prettify): Client<'cookie'> { - return new ClientRuntime<'cookie'>() + const client = new ClientRuntime<'cookie'>() {%~ if sdk.platform == 'console' %} - .applyBase<'cookie'>({ ...params, mode: 'admin' }, 'server') + .applyBase<'cookie'>({ ...params, mode: 'admin' }, 'server'); {%~ else %} - .applyBase<'cookie'>(params, 'server') + .applyBase<'cookie'>(params, 'server'); {%~ endif %} - .setCookie(params.cookie); + client.headers['Cookie'] = params.cookie; + client.config.cookie = params.cookie; + return client; } static fromJWT(params: Prettify): Client<'jwt'> { - return new ClientRuntime<'jwt'>() - .applyBase<'jwt'>(params, 'server') - .setJWT(params.jwt); + const client = new ClientRuntime<'jwt'>() + .applyBase<'jwt'>(params, 'server'); + client.headers['X-{{ spec.title | caseUcfirst }}-JWT'] = params.jwt; + client.config.jwt = params.jwt; + return client; } static fromDevKey(params: Prettify): Client<'devKey'> { - return new ClientRuntime<'devKey'>() - .applyBase<'devKey'>(params, 'client') - .setDevKey(params.devKey); + const client = new ClientRuntime<'devKey'>() + .applyBase<'devKey'>(params, 'client'); + client.headers['X-{{ spec.title | caseUcfirst }}-Dev-Key'] = params.devKey; + client.config.devkey = params.devKey; + return client; } static fromImpersonation(params: Prettify): Client<'impersonation'> { @@ -473,16 +482,24 @@ class ClientRuntime { } const client = new ClientRuntime<'impersonation'>() - .applyBase<'impersonation'>(params, 'client') - .setSession(params.session); + .applyBase<'impersonation'>(params, 'client'); + + client.headers['X-{{ spec.title | caseUcfirst }}-Session'] = params.session; + client.config.session = params.session; if (params.userId !== undefined) { - return client.setImpersonateUserId(params.userId); + client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id'] = params.userId; + client.config.impersonateuserid = params.userId; + return client; } if (params.email !== undefined) { - return client.setImpersonateUserEmail(params.email); + client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email'] = params.email; + client.config.impersonateuseremail = params.email; + return client; } - return client.setImpersonateUserPhone(params.phone); + client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone'] = params.phone; + client.config.impersonateuserphone = params.phone; + return client; } withJWT(jwt: string): this { diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index 958c56a01a..19693ecd9a 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -1,6 +1,8 @@ {# Detect service shape and imports before emitting TypeScript. #} {% set auth = service | webServiceAuth %} +{%~ if auth.needsServiceFlatten %} import { Service } from '../service'; +{%~ endif %} import { {{ spec.title | caseUcfirst}}Exception, Client, {% if auth.needsClientAuth or auth.hasMixedTier %}type ClientAuth, {% endif %}{% if auth.needsServerAuth or auth.hasMixedTier %}type ServerAuth, {% endif %}type Payload{% if auth.hasUpload %}, UploadProgress{% endif %} } from '../client'; import type { Models } from '../models'; From e3b701e63a22f447e7883445840d7ef53266a906 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 17:58:28 +0530 Subject: [PATCH 53/69] Fix web auth config typing --- templates/web/src/client.ts.twig | 2 +- templates/web/src/services/template.ts.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 8c7635d6fb..71702de1ae 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -438,7 +438,7 @@ class ClientRuntime { const client = new ClientRuntime<'apiKey'>() .applyBase<'apiKey'>(params, 'server'); client.headers['X-{{ spec.title | caseUcfirst }}-Key'] = params.apiKey; - client.config.key = params.apiKey; + (client.config as unknown as Record).key = params.apiKey; return client; } diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index 19693ecd9a..9ead9af831 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -171,7 +171,7 @@ class {{ service.name | caseUcfirst }}Runtime { {%~ if method.auth|length > 0 %} {%~ for node in method.auth %} {%~ for key,header in node|keys %} - payload['{{header|caseLower}}'] = this.client.config.{{header|caseLower}}; + payload['{{header|caseLower}}'] = (this.client.config as unknown as Record)['{{header|caseLower}}']; {%~ endfor %} {%~ endfor %} {%~ endif %} From 0faf4d94769a15b83e429c22c0d775312086b82d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 18:23:53 +0530 Subject: [PATCH 54/69] Mirror auth factory values into config --- templates/dart/lib/src/client.dart.twig | 9 ++++++++ .../main/kotlin/io/appwrite/Client.kt.twig | 21 ++++++++++++++++--- templates/react-native/src/client.ts.twig | 5 +++++ tests/languages/dart/tests.dart | 10 ++++----- tests/languages/kotlin/Tests.kt | 10 ++++----- 5 files changed, 42 insertions(+), 13 deletions(-) diff --git a/templates/dart/lib/src/client.dart.twig b/templates/dart/lib/src/client.dart.twig index 4c04f70796..5a870ad46f 100644 --- a/templates/dart/lib/src/client.dart.twig +++ b/templates/dart/lib/src/client.dart.twig @@ -92,6 +92,7 @@ abstract class Client { selfSigned: selfSigned, sdkPlatform: 'server', ); + client.config['cookie'] = cookie; client.addHeader('Cookie', cookie); return client; } @@ -130,6 +131,8 @@ abstract class Client { selfSigned: selfSigned, sdkPlatform: 'client', ); + client.config['devKey'] = devKey; + client.config['devkey'] = devKey; client.addHeader('X-{{ spec.title | caseUcfirst }}-Dev-Key', devKey); return client; } @@ -162,10 +165,16 @@ abstract class Client { client.setSession(session); if (userId != null) { + client.config['impersonateUserId'] = userId; + client.config['impersonateuserid'] = userId; client.addHeader('X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id', userId); } else if (userEmail != null) { + client.config['impersonateUserEmail'] = userEmail; + client.config['impersonateuseremail'] = userEmail; client.addHeader('X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email', userEmail); } else { + client.config['impersonateUserPhone'] = userPhone!; + client.config['impersonateuserphone'] = userPhone!; client.addHeader('X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone', userPhone!); } diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig index b06ee5b41a..158551d32e 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig @@ -108,6 +108,7 @@ class Client @JvmOverloads constructor( selfSigned = selfSigned, sdkPlatform = "server" ) + client.config["cookie"] = cookie client.addHeader("Cookie", cookie) return client } @@ -145,6 +146,8 @@ class Client @JvmOverloads constructor( locale = locale, selfSigned = selfSigned ) + client.config["devKey"] = devKey + client.config["devkey"] = devKey client.addHeader("X-{{ spec.title | caseUcfirst }}-Dev-Key", devKey) return client } @@ -174,9 +177,21 @@ class Client @JvmOverloads constructor( ).setSession(session) when { - userId != null -> client.addHeader("X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id", userId) - userEmail != null -> client.addHeader("X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email", userEmail) - userPhone != null -> client.addHeader("X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone", userPhone) + userId != null -> { + client.config["impersonateUserId"] = userId + client.config["impersonateuserid"] = userId + client.addHeader("X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id", userId) + } + userEmail != null -> { + client.config["impersonateUserEmail"] = userEmail + client.config["impersonateuseremail"] = userEmail + client.addHeader("X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email", userEmail) + } + userPhone != null -> { + client.config["impersonateUserPhone"] = userPhone + client.config["impersonateuserphone"] = userPhone + client.addHeader("X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone", userPhone) + } } return client diff --git a/templates/react-native/src/client.ts.twig b/templates/react-native/src/client.ts.twig index 180b081b59..91c658becb 100644 --- a/templates/react-native/src/client.ts.twig +++ b/templates/react-native/src/client.ts.twig @@ -207,12 +207,14 @@ class Client { static fromCookie(params: BaseClientParams & { cookie: string }): Client { const client = new Client().applyBase(params); + client.config.cookie = params.cookie; client.headers['Cookie'] = params.cookie; return client; } static fromDevKey(params: BaseClientParams & { devKey: string }): Client { const client = new Client().applyBase(params); + client.config.devkey = params.devKey; client.headers['X-{{ spec.title | caseUcfirst }}-Dev-Key'] = params.devKey; return client; } @@ -233,10 +235,13 @@ class Client { .setSession(params.session); if (params.userId !== undefined) { + client.config.impersonateuserid = params.userId; client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id'] = params.userId; } else if (params.email !== undefined) { + client.config.impersonateuseremail = params.email; client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email'] = params.email; } else { + client.config.impersonateuserphone = params.phone; client.headers['X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone'] = params.phone; } diff --git a/tests/languages/dart/tests.dart b/tests/languages/dart/tests.dart index 02f04cf8a3..69a7aea803 100644 --- a/tests/languages/dart/tests.dart +++ b/tests/languages/dart/tests.dart @@ -38,7 +38,7 @@ void main() async { projectId: 'auth-project', cookie: 'auth-cookie', ); - authFactoryOutputs.add(cookieClient.getHeaders()['Cookie']); + authFactoryOutputs.add(cookieClient.config['cookie']); final jwtClient = Client.fromJWT( endPoint: 'https://cloud.appwrite.io/v1', @@ -52,7 +52,7 @@ void main() async { projectId: 'auth-project', devKey: 'auth-dev-key', ); - authFactoryOutputs.add(devKeyClient.getHeaders()['X-Appwrite-Dev-Key']); + authFactoryOutputs.add(devKeyClient.config['devKey']); final impersonationUserClient = Client.fromImpersonation( endPoint: 'https://cloud.appwrite.io/v1', @@ -60,7 +60,7 @@ void main() async { session: 'auth-session', userId: 'auth-user-id', ); - authFactoryOutputs.add(impersonationUserClient.getHeaders()['X-Appwrite-Impersonate-User-Id']); + authFactoryOutputs.add(impersonationUserClient.config['impersonateUserId']); final impersonationEmailClient = Client.fromImpersonation( endPoint: 'https://cloud.appwrite.io/v1', @@ -68,7 +68,7 @@ void main() async { session: 'auth-session', userEmail: 'auth@example.com', ); - authFactoryOutputs.add(impersonationEmailClient.getHeaders()['X-Appwrite-Impersonate-User-Email']); + authFactoryOutputs.add(impersonationEmailClient.config['impersonateUserEmail']); final impersonationPhoneClient = Client.fromImpersonation( endPoint: 'https://cloud.appwrite.io/v1', @@ -76,7 +76,7 @@ void main() async { session: 'auth-session', userPhone: '+15555550123', ); - authFactoryOutputs.add(impersonationPhoneClient.getHeaders()['X-Appwrite-Impersonate-User-Phone']); + authFactoryOutputs.add(impersonationPhoneClient.config['impersonateUserPhone']); try { Client.fromImpersonation( diff --git a/tests/languages/kotlin/Tests.kt b/tests/languages/kotlin/Tests.kt index 3d52bd1159..5348e25fab 100644 --- a/tests/languages/kotlin/Tests.kt +++ b/tests/languages/kotlin/Tests.kt @@ -72,7 +72,7 @@ class ServiceTest { cookie = "auth-cookie", endPoint = "https://cloud.appwrite.io/v1" ) - authFactoryOutputs.add(cookieClient.getHeaders()["Cookie"]) + authFactoryOutputs.add(cookieClient.config["cookie"]) val jwtClient = Client.fromJWT( projectId = "auth-project", @@ -86,7 +86,7 @@ class ServiceTest { devKey = "auth-dev-key", endPoint = "https://cloud.appwrite.io/v1" ) - authFactoryOutputs.add(devKeyClient.getHeaders()["X-Appwrite-Dev-Key"]) + authFactoryOutputs.add(devKeyClient.config["devKey"]) val impersonationUserClient = Client.fromImpersonation( projectId = "auth-project", @@ -94,7 +94,7 @@ class ServiceTest { userId = "auth-user-id", endPoint = "https://cloud.appwrite.io/v1" ) - authFactoryOutputs.add(impersonationUserClient.getHeaders()["X-Appwrite-Impersonate-User-Id"]) + authFactoryOutputs.add(impersonationUserClient.config["impersonateUserId"]) val impersonationEmailClient = Client.fromImpersonation( projectId = "auth-project", @@ -102,7 +102,7 @@ class ServiceTest { userEmail = "auth@example.com", endPoint = "https://cloud.appwrite.io/v1" ) - authFactoryOutputs.add(impersonationEmailClient.getHeaders()["X-Appwrite-Impersonate-User-Email"]) + authFactoryOutputs.add(impersonationEmailClient.config["impersonateUserEmail"]) val impersonationPhoneClient = Client.fromImpersonation( projectId = "auth-project", @@ -110,7 +110,7 @@ class ServiceTest { userPhone = "+15555550123", endPoint = "https://cloud.appwrite.io/v1" ) - authFactoryOutputs.add(impersonationPhoneClient.getHeaders()["X-Appwrite-Impersonate-User-Phone"]) + authFactoryOutputs.add(impersonationPhoneClient.config["impersonateUserPhone"]) try { Client.fromImpersonation( From b3f10bdf680aa5d21089fd606cbb7075789b8e14 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 7 May 2026 18:49:54 +0530 Subject: [PATCH 55/69] Mirror Swift auth factories into config --- templates/swift/Sources/Client.swift.twig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index f8aa4352a1..3eae539d10 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -100,6 +100,7 @@ open class Client { ) throws -> Client { let client = Client() try configure(client, endpoint: endpoint, projectId: projectId, locale: locale, selfSigned: selfSigned, sdkPlatform: "server") + client.config["cookie"] = cookie client.headers["Cookie"] = cookie return client } @@ -125,6 +126,7 @@ open class Client { selfSigned: Bool = false ) throws -> Client { let client = try fromBrowser(endpoint: endpoint, projectId: projectId, locale: locale, selfSigned: selfSigned) + client.config["devkey"] = devKey client.headers["X-{{ spec.title | caseUcfirst }}-Dev-Key"] = devKey return client } @@ -148,10 +150,13 @@ open class Client { client.setSession(session) if let userId = userId { + client.config["impersonateuserid"] = userId client.headers["X-{{ spec.title | caseUcfirst }}-Impersonate-User-Id"] = userId } else if let userEmail = userEmail { + client.config["impersonateuseremail"] = userEmail client.headers["X-{{ spec.title | caseUcfirst }}-Impersonate-User-Email"] = userEmail } else if let userPhone = userPhone { + client.config["impersonateuserphone"] = userPhone client.headers["X-{{ spec.title | caseUcfirst }}-Impersonate-User-Phone"] = userPhone } From f24d0be95f944bf9df0a15ca014eee9c94f722d5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 11 May 2026 14:44:04 +0530 Subject: [PATCH 56/69] Use Android client config property --- .../library/src/main/java/io/package/Client.kt.twig | 11 +---------- .../main/java/io/package/services/Realtime.kt.twig | 2 +- .../src/main/java/io/package/services/Service.kt.twig | 4 ++-- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Client.kt.twig b/templates/android/library/src/main/java/io/package/Client.kt.twig index 63ad4b240a..762e61e31f 100644 --- a/templates/android/library/src/main/java/io/package/Client.kt.twig +++ b/templates/android/library/src/main/java/io/package/Client.kt.twig @@ -40,8 +40,7 @@ import kotlin.coroutines.resume interface ClientAuth { val endpoint: String val endpointRealtime: String? - - fun getConfig(key: String): String? + val config: MutableMap fun getHeaders(): Map @@ -499,14 +498,6 @@ class Client @JvmOverloads constructor( */ override fun getHeaders(): Map = headers.toMap() - /** - * Get a configuration value by key. - * - * @param key the configuration key - * @return the configuration value if set - */ - override fun getConfig(key: String): String? = config[key] - /** * Get the cookies for a given URL from the SDK's cookie store. * diff --git a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig index 98b31d59a6..f2d5b5b66f 100644 --- a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig @@ -64,7 +64,7 @@ class Realtime(private val client: ClientAuth) : Service(client), CoroutineScope return } - val encodedProject = java.net.URLEncoder.encode(client.getConfig("project").toString(), "UTF-8") + val encodedProject = java.net.URLEncoder.encode(client.config["project"].toString(), "UTF-8") val queryParams = "project=$encodedProject" val url = "${client.endpointRealtime}/realtime?$queryParams" request = Request.Builder().url(url).build() diff --git a/templates/android/library/src/main/java/io/package/services/Service.kt.twig b/templates/android/library/src/main/java/io/package/services/Service.kt.twig index 3401a0a851..0284223c48 100644 --- a/templates/android/library/src/main/java/io/package/services/Service.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/Service.kt.twig @@ -82,7 +82,7 @@ class {{ service.name | caseUcfirst }}(private val client: ClientAuth) : Service {%~ if method.auth | length > 0 %} {%~ for node in method.auth %} {%~ for key,header in node | keys %} - "{{ header | caseLower }}" to client.getConfig("{{ header | caseLower }}"), + "{{ header | caseLower }}" to client.config["{{ header | caseLower }}"], {%~ endfor %} {%~ endfor %} {%~ endif %} @@ -107,7 +107,7 @@ class {{ service.name | caseUcfirst }}(private val client: ClientAuth) : Service } val apiUrl = Uri.parse("${client.endpoint}${apiPath}?${apiQuery.joinToString("&")}") - val callbackUrlScheme = "{{ spec.title | caseLower }}-callback-${client.getConfig("project")}" + val callbackUrlScheme = "{{ spec.title | caseLower }}-callback-${client.config["project"]}" WebAuthComponent.authenticate(activity, apiUrl, callbackUrlScheme) { if (it.isFailure) { From f5a8b33e6e560e842fef18fb609a22c3e80b7e5c Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 11 May 2026 14:56:32 +0530 Subject: [PATCH 57/69] Fix Android ClientAuth config override --- .../android/library/src/main/java/io/package/Client.kt.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/android/library/src/main/java/io/package/Client.kt.twig b/templates/android/library/src/main/java/io/package/Client.kt.twig index 762e61e31f..23f4912601 100644 --- a/templates/android/library/src/main/java/io/package/Client.kt.twig +++ b/templates/android/library/src/main/java/io/package/Client.kt.twig @@ -324,7 +324,7 @@ class Client @JvmOverloads constructor( internal val headers: MutableMap - val config: MutableMap + override val config: MutableMap internal val cookieJar = ListenableCookieJar(CookieManager( SharedPreferencesCookieStore(context.getSharedPreferences(COOKIE_PREFS, Context.MODE_PRIVATE)), From d16d121c9798fac23c345213871cfbf0afa7528a Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 11 May 2026 14:57:57 +0530 Subject: [PATCH 58/69] Update Android auth factory tests for config property --- tests/languages/android/Tests.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/languages/android/Tests.kt b/tests/languages/android/Tests.kt index f96a2fd635..edaccab87e 100644 --- a/tests/languages/android/Tests.kt +++ b/tests/languages/android/Tests.kt @@ -81,8 +81,8 @@ class ServiceTest { locale = "en-US", selfSigned = true ) - writeToFile(browserClient.getConfig("project")) - writeToFile(browserClient.getConfig("locale")) + writeToFile(browserClient.config["project"]) + writeToFile(browserClient.config["locale"]) writeToFile(browserClient.endpointRealtime) val sessionClient = Client.fromSession( @@ -91,7 +91,7 @@ class ServiceTest { session = "auth-session", endpoint = "https://cloud.appwrite.io/v1" ) - writeToFile(sessionClient.getConfig("session")) + writeToFile(sessionClient.config["session"]) val devKeyClient = Client.fromDevKey( context = ApplicationProvider.getApplicationContext(), @@ -99,7 +99,7 @@ class ServiceTest { devKey = "auth-dev-key", endpoint = "https://cloud.appwrite.io/v1" ) - writeToFile(devKeyClient.getConfig("devKey")) + writeToFile(devKeyClient.config["devKey"]) val impersonationUserClient = Client.fromImpersonation( context = ApplicationProvider.getApplicationContext(), @@ -108,7 +108,7 @@ class ServiceTest { userId = "auth-user-id", endpoint = "https://cloud.appwrite.io/v1" ) - writeToFile(impersonationUserClient.getConfig("impersonateUserId")) + writeToFile(impersonationUserClient.config["impersonateUserId"]) val impersonationEmailClient = Client.fromImpersonation( context = ApplicationProvider.getApplicationContext(), @@ -117,7 +117,7 @@ class ServiceTest { userEmail = "auth@example.com", endpoint = "https://cloud.appwrite.io/v1" ) - writeToFile(impersonationEmailClient.getConfig("impersonateUserEmail")) + writeToFile(impersonationEmailClient.config["impersonateUserEmail"]) val impersonationPhoneClient = Client.fromImpersonation( context = ApplicationProvider.getApplicationContext(), @@ -126,7 +126,7 @@ class ServiceTest { userPhone = "+15555550123", endpoint = "https://cloud.appwrite.io/v1" ) - writeToFile(impersonationPhoneClient.getConfig("impersonateUserPhone")) + writeToFile(impersonationPhoneClient.config["impersonateUserPhone"]) try { Client.fromImpersonation( From b10a74d8620e45cc02313f6ccad22b8213bc1de0 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 11 May 2026 15:04:14 +0530 Subject: [PATCH 59/69] Return concrete Android client from factories --- .../src/main/java/io/package/Client.kt.twig | 98 +++---------------- .../src/main/java/io/package/Service.kt.twig | 4 +- .../java/io/package/services/Realtime.kt.twig | 4 +- .../java/io/package/services/Service.kt.twig | 6 +- 4 files changed, 20 insertions(+), 92 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Client.kt.twig b/templates/android/library/src/main/java/io/package/Client.kt.twig index 23f4912601..e222552c2b 100644 --- a/templates/android/library/src/main/java/io/package/Client.kt.twig +++ b/templates/android/library/src/main/java/io/package/Client.kt.twig @@ -37,82 +37,12 @@ import javax.net.ssl.X509TrustManager import kotlin.coroutines.CoroutineContext import kotlin.coroutines.resume -interface ClientAuth { - val endpoint: String - val endpointRealtime: String? - val config: MutableMap - - fun getHeaders(): Map - - fun getCookies(url: String): List - - fun getHttpClient(): OkHttpClient - - suspend fun ping(): String - - suspend fun callInternal( - method: String, - path: String, - headers: Map, - params: Map, - responseType: Class, - converter: ((Any) -> T)? - ): T - - suspend fun chunkedUploadInternal( - path: String, - headers: MutableMap, - params: MutableMap, - responseType: Class, - converter: ((Any) -> T), - paramName: String, - idParamName: String?, - onProgress: ((UploadProgress) -> Unit)?, - ): T -} - -suspend fun ClientAuth.call( - method: String, - path: String = "", - headers: Map = mapOf(), - params: Map = mapOf(), - responseType: Class, - converter: ((Any) -> T)? = null -): T = callInternal( - method = method, - path = path, - headers = headers, - params = params, - responseType = responseType, - converter = converter -) - -suspend fun ClientAuth.chunkedUpload( - path: String, - headers: MutableMap, - params: MutableMap, - responseType: Class, - converter: ((Any) -> T), - paramName: String, - idParamName: String? = null, - onProgress: ((UploadProgress) -> Unit)? = null, -): T = chunkedUploadInternal( - path = path, - headers = headers, - params = params, - responseType = responseType, - converter = converter, - paramName = paramName, - idParamName = idParamName, - onProgress = onProgress -) - class Client @JvmOverloads constructor( context: Context, - override var endpoint: String = "{{spec.endpoint}}", - override var endpointRealtime: String? = null, + var endpoint: String = "{{spec.endpoint}}", + var endpointRealtime: String? = null, private var selfSigned: Boolean = false -) : ClientAuth, CoroutineScope { +) : CoroutineScope { companion object { /** @@ -131,7 +61,7 @@ class Client @JvmOverloads constructor( endpointRealtime: String? = null, locale: String? = null, selfSigned: Boolean = false - ): ClientAuth = fromBrowserClient( + ): Client = fromBrowserClient( context = context, endpoint = endpoint, projectId = projectId, @@ -207,7 +137,7 @@ class Client @JvmOverloads constructor( endpointRealtime: String? = null, locale: String? = null, selfSigned: Boolean = false - ): ClientAuth { + ): Client { val client = fromBrowserClient( context = context, endpoint = endpoint, @@ -236,7 +166,7 @@ class Client @JvmOverloads constructor( endpointRealtime: String? = null, locale: String? = null, selfSigned: Boolean = false - ): ClientAuth { + ): Client { val client = fromBrowserClient( context = context, endpoint = endpoint, @@ -268,7 +198,7 @@ class Client @JvmOverloads constructor( endpointRealtime: String? = null, locale: String? = null, selfSigned: Boolean = false - ): ClientAuth { + ): Client { val targets = listOfNotNull(userId, userEmail, userPhone).size require(targets == 1) { "Exactly one impersonation target must be provided" @@ -324,7 +254,7 @@ class Client @JvmOverloads constructor( internal val headers: MutableMap - override val config: MutableMap + val config: MutableMap internal val cookieJar = ListenableCookieJar(CookieManager( SharedPreferencesCookieStore(context.getSharedPreferences(COOKIE_PREFS, Context.MODE_PRIVATE)), @@ -496,7 +426,7 @@ class Client @JvmOverloads constructor( * * @return a copy of the current request headers */ - override fun getHeaders(): Map = headers.toMap() + fun getHeaders(): Map = headers.toMap() /** * Get the cookies for a given URL from the SDK's cookie store. @@ -504,21 +434,21 @@ class Client @JvmOverloads constructor( * @param url the URL to retrieve cookies for * @return a list of cookies for the given URL */ - override fun getCookies(url: String): List = cookieJar.loadForRequest(url.toHttpUrl()) + fun getCookies(url: String): List = cookieJar.loadForRequest(url.toHttpUrl()) /** * Get the OkHttpClient instance used by this SDK. * * @return the OkHttpClient instance used by this client */ - override fun getHttpClient(): OkHttpClient = http + fun getHttpClient(): OkHttpClient = http /** * Sends a "ping" request to Appwrite to verify connectivity. * * @return String */ - override suspend fun ping(): String { + suspend fun ping(): String { val apiPath = "/ping" val apiParams = mutableMapOf() val apiHeaders = mutableMapOf("content-type" to "application/json") @@ -560,7 +490,7 @@ class Client @JvmOverloads constructor( converter = converter ) - override suspend fun callInternal( + suspend fun callInternal( method: String, path: String, headers: Map, @@ -673,7 +603,7 @@ class Client @JvmOverloads constructor( onProgress = onProgress ) - override suspend fun chunkedUploadInternal( + suspend fun chunkedUploadInternal( path: String, headers: MutableMap, params: MutableMap, diff --git a/templates/android/library/src/main/java/io/package/Service.kt.twig b/templates/android/library/src/main/java/io/package/Service.kt.twig index 8f0f5f6ca3..47f4dec8bc 100644 --- a/templates/android/library/src/main/java/io/package/Service.kt.twig +++ b/templates/android/library/src/main/java/io/package/Service.kt.twig @@ -1,10 +1,10 @@ package {{ sdk.namespace | caseDot }} -import {{ sdk.namespace | caseDot }}.ClientAuth +import {{ sdk.namespace | caseDot }}.Client /** * Abstract class for services. * * @param client The Appwrite client. */ -abstract class Service(@Suppress("UNUSED_PARAMETER") client: ClientAuth) +abstract class Service(@Suppress("UNUSED_PARAMETER") client: Client) diff --git a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig index f2d5b5b66f..5bde51d234 100644 --- a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig @@ -1,7 +1,7 @@ package {{ sdk.namespace | caseDot }}.services import {{ sdk.namespace | caseDot }}.Service -import {{ sdk.namespace | caseDot }}.ClientAuth +import {{ sdk.namespace | caseDot }}.Client import {{ sdk.namespace | caseDot }}.Channel import {{ sdk.namespace | caseDot }}.ID import {{ sdk.namespace | caseDot }}.Query @@ -25,7 +25,7 @@ import java.util.concurrent.atomic.AtomicInteger import android.util.Log import kotlin.coroutines.CoroutineContext -class Realtime(private val client: ClientAuth) : Service(client), CoroutineScope { +class Realtime(private val client: Client) : Service(client), CoroutineScope { private val job = Job() diff --git a/templates/android/library/src/main/java/io/package/services/Service.kt.twig b/templates/android/library/src/main/java/io/package/services/Service.kt.twig index 0284223c48..294f1ef6cc 100644 --- a/templates/android/library/src/main/java/io/package/services/Service.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/Service.kt.twig @@ -1,10 +1,8 @@ package {{ sdk.namespace | caseDot }}.services import android.net.Uri -import {{ sdk.namespace | caseDot }}.ClientAuth +import {{ sdk.namespace | caseDot }}.Client import {{ sdk.namespace | caseDot }}.Service -import {{ sdk.namespace | caseDot }}.call -import {{ sdk.namespace | caseDot }}.chunkedUpload {% if spec.definitions is not empty %} import {{ sdk.namespace | caseDot }}.models.* {% endif %} @@ -27,7 +25,7 @@ import java.io.File /** * {{ service.description | replace({"\n": "\n * "}) | raw }} */ -class {{ service.name | caseUcfirst }}(private val client: ClientAuth) : Service(client) { +class {{ service.name | caseUcfirst }}(private val client: Client) : Service(client) { {% for method in service.methods %} /** From 4ef7ff4d36228076319b76040847154db8e2cc48 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 11 May 2026 15:15:31 +0530 Subject: [PATCH 60/69] Rename base auth factory to from --- .../src/main/java/io/package/Client.kt.twig | 20 ++++++++--------- templates/apple/Sources/Client.swift.twig | 10 ++++----- templates/dart/lib/src/client.dart.twig | 4 ++-- templates/flutter/lib/src/client.dart.twig | 22 +++++++++---------- .../flutter/lib/src/client_base.dart.twig | 8 +++---- .../flutter/lib/src/client_browser.dart.twig | 8 +++---- templates/flutter/lib/src/client_io.dart.twig | 8 +++---- .../main/kotlin/io/appwrite/Client.kt.twig | 8 +++---- templates/node/src/client.ts.twig | 2 +- templates/react-native/src/client.ts.twig | 2 +- templates/swift/Sources/Client.swift.twig | 8 +++---- templates/web/src/client.ts.twig | 6 ++--- tests/languages/android/Tests.kt | 12 +++++----- tests/languages/apple/Tests.swift | 12 +++++----- tests/languages/dart/tests.dart | 12 +++++----- tests/languages/flutter/tests.dart | 12 +++++----- tests/languages/kotlin/Tests.kt | 12 +++++----- tests/languages/node/test.js | 12 +++++----- tests/languages/swift/Tests.swift | 12 +++++----- tests/languages/web/index.html | 16 +++++++------- tests/languages/web/node.js | 16 +++++++------- 21 files changed, 111 insertions(+), 111 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Client.kt.twig b/templates/android/library/src/main/java/io/package/Client.kt.twig index e222552c2b..1093cebcc5 100644 --- a/templates/android/library/src/main/java/io/package/Client.kt.twig +++ b/templates/android/library/src/main/java/io/package/Client.kt.twig @@ -54,14 +54,14 @@ class Client @JvmOverloads constructor( @JvmStatic @JvmOverloads - fun fromBrowser( + fun from( context: Context, projectId: String, endpoint: String = "{{spec.endpoint}}", endpointRealtime: String? = null, locale: String? = null, selfSigned: Boolean = false - ): Client = fromBrowserClient( + ): Client = fromClient( context = context, endpoint = endpoint, projectId = projectId, @@ -70,7 +70,7 @@ class Client @JvmOverloads constructor( selfSigned = selfSigned ) - private fun fromBrowserClient( + private fun fromClient( context: Context, endpoint: String, projectId: String, @@ -138,7 +138,7 @@ class Client @JvmOverloads constructor( locale: String? = null, selfSigned: Boolean = false ): Client { - val client = fromBrowserClient( + val client = fromClient( context = context, endpoint = endpoint, projectId = projectId, @@ -167,7 +167,7 @@ class Client @JvmOverloads constructor( locale: String? = null, selfSigned: Boolean = false ): Client { - val client = fromBrowserClient( + val client = fromClient( context = context, endpoint = endpoint, projectId = projectId, @@ -204,7 +204,7 @@ class Client @JvmOverloads constructor( "Exactly one impersonation target must be provided" } - val client = fromBrowserClient( + val client = fromClient( context = context, endpoint = endpoint, projectId = projectId, @@ -303,7 +303,7 @@ class Client @JvmOverloads constructor( * * @return this */ - @Deprecated("Use Client.fromBrowser or another factory method instead.") + @Deprecated("Use Client.from or another factory method instead.") fun set{{header.key | caseUcfirst}}(value: String): Client { config["{{ header.key | caseCamel }}"] = value {%~ if (header.key | caseCamel) != (header.key | caseLower) %} @@ -321,7 +321,7 @@ class Client @JvmOverloads constructor( * * @return this */ - @Deprecated("Use Client.fromBrowser or another factory method instead.") + @Deprecated("Use Client.from or another factory method instead.") fun setSelfSigned(status: Boolean): Client { applySelfSigned(status) return this @@ -378,7 +378,7 @@ class Client @JvmOverloads constructor( * @return this */ @Throws(IllegalArgumentException::class) - @Deprecated("Use Client.fromBrowser or another factory method instead.") + @Deprecated("Use Client.from or another factory method instead.") fun setEndpoint(endpoint: String): Client { require(endpoint.startsWith("http://") || endpoint.startsWith("https://")) { "Invalid endpoint URL: $endpoint" @@ -398,7 +398,7 @@ class Client @JvmOverloads constructor( * @return this */ @Throws(IllegalArgumentException::class) - @Deprecated("Use Client.fromBrowser or another factory method instead.") + @Deprecated("Use Client.from or another factory method instead.") fun setEndpointRealtime(endpoint: String): Client { require(endpoint.startsWith("ws://") || endpoint.startsWith("wss://")) { "Invalid realtime endpoint URL: $endpoint" diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index 88315a7817..5bd254781e 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -123,7 +123,7 @@ open class Client: ClientAuth { addOriginHeader() } - public static func fromBrowser( + public static func from( endpoint: String = "{{spec.endpoint}}", projectId: String, endpointRealtime: String? = nil, @@ -367,7 +367,7 @@ open class Client: ClientAuth { /// /// @return Client /// - @available(*, deprecated, message: "Use Client.fromBrowser, Client.fromSession, Client.fromDevKey, or Client.fromImpersonation instead.") + @available(*, deprecated, message: "Use Client.from, Client.fromSession, Client.fromDevKey, or Client.fromImpersonation instead.") open func set{{ header.key | caseUcfirst }}(_ value: String) -> Client { config["{{ header.key | caseLower }}"] = value _ = addHeader(key: "{{header.name}}", value: value) @@ -383,7 +383,7 @@ open class Client: ClientAuth { /// /// @return Client /// - @available(*, deprecated, message: "Use Client.fromBrowser, Client.fromSession, Client.fromDevKey, or Client.fromImpersonation instead.") + @available(*, deprecated, message: "Use Client.from, Client.fromSession, Client.fromDevKey, or Client.fromImpersonation instead.") open func setSelfSigned(_ status: Bool = true) -> Client { configureSelfSigned(status) return self @@ -413,7 +413,7 @@ open class Client: ClientAuth { /// /// @return Client /// - @available(*, deprecated, message: "Use Client.fromBrowser, Client.fromSession, Client.fromDevKey, or Client.fromImpersonation instead.") + @available(*, deprecated, message: "Use Client.from, Client.fromSession, Client.fromDevKey, or Client.fromImpersonation instead.") open func setEndpoint(_ endPoint: String) -> Client { try! configureEndpoint(endPoint) return self @@ -426,7 +426,7 @@ open class Client: ClientAuth { /// /// @return Client /// - @available(*, deprecated, message: "Use Client.fromBrowser, Client.fromSession, Client.fromDevKey, or Client.fromImpersonation instead.") + @available(*, deprecated, message: "Use Client.from, Client.fromSession, Client.fromDevKey, or Client.fromImpersonation instead.") open func setEndpointRealtime(_ endPoint: String) -> Client { try! configureEndpointRealtime(endPoint) return self diff --git a/templates/dart/lib/src/client.dart.twig b/templates/dart/lib/src/client.dart.twig index 5a870ad46f..5a96de66f4 100644 --- a/templates/dart/lib/src/client.dart.twig +++ b/templates/dart/lib/src/client.dart.twig @@ -24,8 +24,8 @@ abstract class Client { bool selfSigned = false, }) => createClient(endPoint: endPoint, selfSigned: selfSigned); - /// Creates a client configured for browser-style authentication. - static Client fromBrowser({ + /// Creates a client configured with project authentication. + static Client from({ String endPoint = '{{ spec.endpoint }}', required String projectId, String? locale, diff --git a/templates/flutter/lib/src/client.dart.twig b/templates/flutter/lib/src/client.dart.twig index ac4c4ccf4d..61c772b321 100644 --- a/templates/flutter/lib/src/client.dart.twig +++ b/templates/flutter/lib/src/client.dart.twig @@ -63,14 +63,14 @@ abstract class Client implements ClientAuth { bool selfSigned = false, }) => createClient(endPoint: endPoint, selfSigned: selfSigned); - /// Initializes a client for browser/mobile session authentication. - static ClientAuth fromBrowser({ + /// Initializes a client with project authentication. + static ClientAuth from({ String endPoint = '{{ spec.endpoint }}', required String projectId, String? endPointRealtime, String? locale, bool selfSigned = false, - }) => _fromBrowserClient( + }) => _fromClient( endPoint: endPoint, projectId: projectId, endPointRealtime: endPointRealtime, @@ -78,7 +78,7 @@ abstract class Client implements ClientAuth { selfSigned: selfSigned, ); - static Client _fromBrowserClient({ + static Client _fromClient({ required String endPoint, required String projectId, String? endPointRealtime, @@ -145,7 +145,7 @@ abstract class Client implements ClientAuth { String? locale, bool selfSigned = false, }) { - final client = _fromBrowserClient( + final client = _fromClient( endPoint: endPoint, projectId: projectId, endPointRealtime: endPointRealtime, @@ -171,7 +171,7 @@ abstract class Client implements ClientAuth { String? locale, bool selfSigned = false, }) { - final client = _fromBrowserClient( + final client = _fromClient( endPoint: endPoint, projectId: projectId, endPointRealtime: endPointRealtime, @@ -207,7 +207,7 @@ abstract class Client implements ClientAuth { ); } - final client = _fromBrowserClient( + final client = _fromClient( endPoint: endPoint, projectId: projectId, endPointRealtime: endPointRealtime, @@ -253,15 +253,15 @@ abstract class Client implements ClientAuth { /// If self signed is true, [Client] will ignore invalid certificates. /// This is helpful in environments where your {{spec.title | caseUcfirst}} /// instance does not have a valid SSL certificate. - @Deprecated('Use Client.fromBrowser or another factory constructor instead.') + @Deprecated('Use Client.from or another factory constructor instead.') Client setSelfSigned({bool status = true}); /// Set the {{spec.title | caseUcfirst}} endpoint. - @Deprecated('Use Client.fromBrowser or another factory constructor instead.') + @Deprecated('Use Client.from or another factory constructor instead.') Client setEndpoint(String endPoint); /// Set the {{spec.title | caseUcfirst}} realtime endpoint. - @Deprecated('Use Client.fromBrowser or another factory constructor instead.') + @Deprecated('Use Client.from or another factory constructor instead.') Client setEndPointRealtime(String endPoint); {% for header in spec.global.headers %} @@ -270,7 +270,7 @@ abstract class Client implements ClientAuth { /// /// {{header.description}}. {% endif %} - @Deprecated('Use Client.fromBrowser or another factory constructor instead.') + @Deprecated('Use Client.from or another factory constructor instead.') Client set{{header.key | caseUcfirst}}(String value); {% endfor %} diff --git a/templates/flutter/lib/src/client_base.dart.twig b/templates/flutter/lib/src/client_base.dart.twig index c9bf944c84..bd34f83023 100644 --- a/templates/flutter/lib/src/client_base.dart.twig +++ b/templates/flutter/lib/src/client_base.dart.twig @@ -7,20 +7,20 @@ abstract class ClientBase implements Client { {% if header.description %} /// {{header.description}} {% endif %} - @Deprecated('Use Client.fromBrowser or another factory constructor instead.') + @Deprecated('Use Client.from or another factory constructor instead.') @override ClientBase set{{header.key | caseUcfirst}}(value); {% endfor %} - @Deprecated('Use Client.fromBrowser or another factory constructor instead.') + @Deprecated('Use Client.from or another factory constructor instead.') @override ClientBase setSelfSigned({bool status = true}); - @Deprecated('Use Client.fromBrowser or another factory constructor instead.') + @Deprecated('Use Client.from or another factory constructor instead.') @override ClientBase setEndpoint(String endPoint); - @Deprecated('Use Client.fromBrowser or another factory constructor instead.') + @Deprecated('Use Client.from or another factory constructor instead.') @override Client setEndPointRealtime(String endPoint); diff --git a/templates/flutter/lib/src/client_browser.dart.twig b/templates/flutter/lib/src/client_browser.dart.twig index 89b6c890a9..4c9aa25af6 100644 --- a/templates/flutter/lib/src/client_browser.dart.twig +++ b/templates/flutter/lib/src/client_browser.dart.twig @@ -65,7 +65,7 @@ class ClientBrowser extends ClientBase with ClientMixin { {% if header.description %} /// {{header.description}} {% endif %} - @Deprecated('Use Client.fromBrowser or another factory constructor instead.') + @Deprecated('Use Client.from or another factory constructor instead.') @override ClientBrowser set{{header.key | caseUcfirst}}(value) { config['{{ header.key | caseCamel }}'] = value; @@ -74,13 +74,13 @@ class ClientBrowser extends ClientBase with ClientMixin { } {% endfor %} - @Deprecated('Use Client.fromBrowser or another factory constructor instead.') + @Deprecated('Use Client.from or another factory constructor instead.') @override ClientBrowser setSelfSigned({bool status = true}) { return this; } - @Deprecated('Use Client.fromBrowser or another factory constructor instead.') + @Deprecated('Use Client.from or another factory constructor instead.') @override ClientBrowser setEndpoint(String endPoint) { if (!endPoint.startsWith('http://') && !endPoint.startsWith('https://')) { @@ -95,7 +95,7 @@ class ClientBrowser extends ClientBase with ClientMixin { return this; } - @Deprecated('Use Client.fromBrowser or another factory constructor instead.') + @Deprecated('Use Client.from or another factory constructor instead.') @override ClientBrowser setEndPointRealtime(String endPoint) { if (!endPoint.startsWith('ws://') && !endPoint.startsWith('wss://')) { diff --git a/templates/flutter/lib/src/client_io.dart.twig b/templates/flutter/lib/src/client_io.dart.twig index 4df85928ae..512614d81b 100644 --- a/templates/flutter/lib/src/client_io.dart.twig +++ b/templates/flutter/lib/src/client_io.dart.twig @@ -91,7 +91,7 @@ class ClientIO extends ClientBase with ClientMixin { {% if header.description %} /// {{header.description}} {% endif %} - @Deprecated('Use Client.fromBrowser or another factory constructor instead.') + @Deprecated('Use Client.from or another factory constructor instead.') @override ClientIO set{{header.key | caseUcfirst}}(value) { config['{{ header.key | caseCamel }}'] = value; @@ -100,7 +100,7 @@ class ClientIO extends ClientBase with ClientMixin { } {% endfor %} - @Deprecated('Use Client.fromBrowser or another factory constructor instead.') + @Deprecated('Use Client.from or another factory constructor instead.') @override ClientIO setSelfSigned({bool status = true}) { selfSigned = status; @@ -109,7 +109,7 @@ class ClientIO extends ClientBase with ClientMixin { return this; } - @Deprecated('Use Client.fromBrowser or another factory constructor instead.') + @Deprecated('Use Client.from or another factory constructor instead.') @override ClientIO setEndpoint(String endPoint) { if (!endPoint.startsWith('http://') && !endPoint.startsWith('https://')) { @@ -124,7 +124,7 @@ class ClientIO extends ClientBase with ClientMixin { return this; } - @Deprecated('Use Client.fromBrowser or another factory constructor instead.') + @Deprecated('Use Client.from or another factory constructor instead.') @override ClientIO setEndPointRealtime(String endPoint) { if (!endPoint.startsWith('ws://') && !endPoint.startsWith('wss://')) { diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig index 158551d32e..5f54051d34 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig @@ -42,7 +42,7 @@ class Client @JvmOverloads constructor( @JvmStatic @JvmOverloads - fun fromBrowser( + fun from( projectId: String, endPoint: String = "{{spec.endpoint}}", locale: String? = null, @@ -66,7 +66,7 @@ class Client @JvmOverloads constructor( locale: String? = null, selfSigned: Boolean = false ): Client { - return fromBrowser( + return from( projectId = projectId, endPoint = endPoint, locale = locale, @@ -140,7 +140,7 @@ class Client @JvmOverloads constructor( locale: String? = null, selfSigned: Boolean = false ): Client { - val client = fromBrowser( + val client = from( projectId = projectId, endPoint = endPoint, locale = locale, @@ -169,7 +169,7 @@ class Client @JvmOverloads constructor( "Exactly one impersonation target must be provided" } - val client = fromBrowser( + val client = from( projectId = projectId, endPoint = endPoint, locale = locale, diff --git a/templates/node/src/client.ts.twig b/templates/node/src/client.ts.twig index bca4842704..62355ff673 100644 --- a/templates/node/src/client.ts.twig +++ b/templates/node/src/client.ts.twig @@ -142,7 +142,7 @@ class Client { {%~ endfor %} }; - static fromBrowser(params: BaseClientParams): Client { + static from(params: BaseClientParams): Client { return new Client().applyBase(params, 'client'); } diff --git a/templates/react-native/src/client.ts.twig b/templates/react-native/src/client.ts.twig index 91c658becb..e9ad82cdf5 100644 --- a/templates/react-native/src/client.ts.twig +++ b/templates/react-native/src/client.ts.twig @@ -195,7 +195,7 @@ class Client { {% endfor %} }; - static fromBrowser(params: BaseClientParams): Client { + static from(params: BaseClientParams): Client { return new Client().applyBase(params); } diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index 3eae539d10..001894f4d6 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -55,7 +55,7 @@ open class Client { addOriginHeader() } - public static func fromBrowser( + public static func from( endpoint: String = "{{spec.endpoint}}", projectId: String, locale: String? = nil, @@ -73,7 +73,7 @@ open class Client { locale: String? = nil, selfSigned: Bool = false ) throws -> Client { - let client = try fromBrowser(endpoint: endpoint, projectId: projectId, locale: locale, selfSigned: selfSigned) + let client = try from(endpoint: endpoint, projectId: projectId, locale: locale, selfSigned: selfSigned) client.setSession(session) return client } @@ -125,7 +125,7 @@ open class Client { locale: String? = nil, selfSigned: Bool = false ) throws -> Client { - let client = try fromBrowser(endpoint: endpoint, projectId: projectId, locale: locale, selfSigned: selfSigned) + let client = try from(endpoint: endpoint, projectId: projectId, locale: locale, selfSigned: selfSigned) client.config["devkey"] = devKey client.headers["X-{{ spec.title | caseUcfirst }}-Dev-Key"] = devKey return client @@ -146,7 +146,7 @@ open class Client { throw {{ spec.title | caseUcfirst }}Error(message: "Exactly one impersonation target must be provided") } - let client = try fromBrowser(endpoint: endpoint, projectId: projectId, locale: locale, selfSigned: selfSigned) + let client = try from(endpoint: endpoint, projectId: projectId, locale: locale, selfSigned: selfSigned) client.setSession(session) if let userId = userId { diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 71702de1ae..1484bb1f82 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -375,7 +375,7 @@ type LegacyClient = Omit, C type ClientConstructor = { new (): LegacyClient; - fromBrowser(params: Prettify): Client<'browser'>; + from(params: Prettify): Client<'browser'>; fromSession(params: Prettify): Client<'session'>; fromAPIKey(params: Prettify): Client<'apiKey'>; fromCookie(params: Prettify): Client<'cookie'>; @@ -422,7 +422,7 @@ class ClientRuntime { {%~ endfor %} }; - static fromBrowser(params: Prettify): Client<'browser'> { + static from(params: Prettify): Client<'browser'> { return new ClientRuntime<'browser'>().applyBase<'browser'>(params, 'client'); } @@ -562,7 +562,7 @@ class ClientRuntime { * @returns {this} */ /** - * @deprecated Use `Client.fromBrowser`, `Client.fromSession`, `Client.fromAPIKey`, or another static factory instead. + * @deprecated Use `Client.from`, `Client.fromSession`, `Client.fromAPIKey`, or another static factory instead. */ setEndpoint(endpoint: string): this { if (!endpoint || typeof endpoint !== 'string') { diff --git a/tests/languages/android/Tests.kt b/tests/languages/android/Tests.kt index edaccab87e..cea80e49f7 100644 --- a/tests/languages/android/Tests.kt +++ b/tests/languages/android/Tests.kt @@ -73,7 +73,7 @@ class ServiceTest { writeToFile("x-sdk-name: ${sdkHeaders["x-sdk-name"]}; x-sdk-platform: ${sdkHeaders["x-sdk-platform"]}; x-sdk-language: ${sdkHeaders["x-sdk-language"]}; x-sdk-version: ${sdkHeaders["x-sdk-version"]}") - val browserClient = Client.fromBrowser( + val baseClient = Client.from( context = ApplicationProvider.getApplicationContext(), projectId = "auth-project", endpoint = "https://cloud.appwrite.io/v1", @@ -81,9 +81,9 @@ class ServiceTest { locale = "en-US", selfSigned = true ) - writeToFile(browserClient.config["project"]) - writeToFile(browserClient.config["locale"]) - writeToFile(browserClient.endpointRealtime) + writeToFile(baseClient.config["project"]) + writeToFile(baseClient.config["locale"]) + writeToFile(baseClient.endpointRealtime) val sessionClient = Client.fromSession( context = ApplicationProvider.getApplicationContext(), @@ -140,7 +140,7 @@ class ServiceTest { } try { - Client.fromBrowser( + Client.from( context = ApplicationProvider.getApplicationContext(), projectId = "auth-project", endpoint = "htp://cloud.appwrite.io/v1" @@ -150,7 +150,7 @@ class ServiceTest { } try { - Client.fromBrowser( + Client.from( context = ApplicationProvider.getApplicationContext(), projectId = "auth-project", endpoint = "https://cloud.appwrite.io/v1", diff --git a/tests/languages/apple/Tests.swift b/tests/languages/apple/Tests.swift index 2e951a4dd8..793bba3634 100644 --- a/tests/languages/apple/Tests.swift +++ b/tests/languages/apple/Tests.swift @@ -27,16 +27,16 @@ class Tests: XCTestCase { let sdkHeaders = client.getHeaders() print("x-sdk-name: \(sdkHeaders["x-sdk-name"] ?? "nil"); x-sdk-platform: \(sdkHeaders["x-sdk-platform"] ?? "nil"); x-sdk-language: \(sdkHeaders["x-sdk-language"] ?? "nil"); x-sdk-version: \(sdkHeaders["x-sdk-version"] ?? "nil")") - let browserClient = try Client.fromBrowser( + let baseClient = try Client.from( endpoint: "https://cloud.appwrite.io/v1", projectId: "auth-project", endpointRealtime: "wss://realtime.example.com/v1", locale: "en-US", selfSigned: true ) - print(browserClient.getConfig(key: "project") ?? "nil") - print(browserClient.getConfig(key: "locale") ?? "nil") - print(browserClient.endPointRealtime ?? "nil") + print(baseClient.getConfig(key: "project") ?? "nil") + print(baseClient.getConfig(key: "locale") ?? "nil") + print(baseClient.endPointRealtime ?? "nil") let sessionClient = try Client.fromSession( endpoint: "https://cloud.appwrite.io/v1", @@ -87,7 +87,7 @@ class Tests: XCTestCase { } do { - _ = try Client.fromBrowser( + _ = try Client.from( endpoint: "htp://cloud.appwrite.io/v1", projectId: "auth-project" ) @@ -96,7 +96,7 @@ class Tests: XCTestCase { } do { - _ = try Client.fromBrowser( + _ = try Client.from( endpoint: "https://cloud.appwrite.io/v1", projectId: "auth-project", endpointRealtime: "ftp://cloud.appwrite.io/v1" diff --git a/tests/languages/dart/tests.dart b/tests/languages/dart/tests.dart index 69a7aea803..dc536c7ad5 100644 --- a/tests/languages/dart/tests.dart +++ b/tests/languages/dart/tests.dart @@ -8,16 +8,16 @@ import '../lib/src/input_file.dart'; void main() async { final authFactoryOutputs = []; - final browserClient = Client.fromBrowser( + final baseClient = Client.from( endPoint: 'https://cloud.appwrite.io/v1', projectId: 'auth-project', locale: 'en-US', selfSigned: true, ); - final browserHeaders = browserClient.getHeaders(); - authFactoryOutputs.add(browserHeaders['X-Appwrite-Project']); - authFactoryOutputs.add(browserHeaders['X-Appwrite-Locale']); - authFactoryOutputs.add(browserHeaders['x-sdk-platform']); + final baseHeaders = baseClient.getHeaders(); + authFactoryOutputs.add(baseHeaders['X-Appwrite-Project']); + authFactoryOutputs.add(baseHeaders['X-Appwrite-Locale']); + authFactoryOutputs.add(baseHeaders['x-sdk-platform']); final sessionClient = Client.fromSession( endPoint: 'https://cloud.appwrite.io/v1', @@ -89,7 +89,7 @@ void main() async { } try { - Client.fromBrowser( + Client.from( endPoint: 'htp://cloud.appwrite.io/v1', projectId: 'auth-project', ); diff --git a/tests/languages/flutter/tests.dart b/tests/languages/flutter/tests.dart index c9604a6b51..7f2fb4b38f 100644 --- a/tests/languages/flutter/tests.dart +++ b/tests/languages/flutter/tests.dart @@ -55,16 +55,16 @@ void main() async { ); final authFactoryOutputs = []; - final browserClient = Client.fromBrowser( + final baseClient = Client.from( endPoint: 'https://cloud.appwrite.io/v1', projectId: 'auth-project', endPointRealtime: 'wss://realtime.example.com/v1', locale: 'en-US', selfSigned: true, ); - authFactoryOutputs.add(browserClient.config['project']); - authFactoryOutputs.add(browserClient.config['locale']); - authFactoryOutputs.add(browserClient.endPointRealtime); + authFactoryOutputs.add(baseClient.config['project']); + authFactoryOutputs.add(baseClient.config['locale']); + authFactoryOutputs.add(baseClient.endPointRealtime); final sessionClient = Client.fromSession( endPoint: 'https://cloud.appwrite.io/v1', @@ -115,7 +115,7 @@ void main() async { } try { - Client.fromBrowser( + Client.from( endPoint: 'htp://cloud.appwrite.io/v1', projectId: 'auth-project', ); @@ -124,7 +124,7 @@ void main() async { } try { - Client.fromBrowser( + Client.from( endPoint: 'https://cloud.appwrite.io/v1', projectId: 'auth-project', endPointRealtime: 'ftp://cloud.appwrite.io/v1', diff --git a/tests/languages/kotlin/Tests.kt b/tests/languages/kotlin/Tests.kt index 5348e25fab..4394b36a04 100644 --- a/tests/languages/kotlin/Tests.kt +++ b/tests/languages/kotlin/Tests.kt @@ -40,17 +40,17 @@ class ServiceTest { @Test @Throws(IOException::class) fun test() { - val browserClient = Client.fromBrowser( + val baseClient = Client.from( projectId = "auth-project", endPoint = "https://cloud.appwrite.io/v1", locale = "en-US", selfSigned = true ) - val browserHeaders = browserClient.getHeaders() + val baseHeaders = baseClient.getHeaders() val authFactoryOutputs = mutableListOf( - browserHeaders["x-appwrite-project"], - browserHeaders["x-appwrite-locale"], - browserHeaders["x-sdk-platform"] + baseHeaders["x-appwrite-project"], + baseHeaders["x-appwrite-locale"], + baseHeaders["x-sdk-platform"] ) val sessionClient = Client.fromSession( @@ -123,7 +123,7 @@ class ServiceTest { } try { - Client.fromBrowser( + Client.from( projectId = "auth-project", endPoint = "htp://cloud.appwrite.io/v1" ) diff --git a/tests/languages/node/test.js b/tests/languages/node/test.js index 7c933412b6..c8ed96eb12 100644 --- a/tests/languages/node/test.js +++ b/tests/languages/node/test.js @@ -18,15 +18,15 @@ async function start() { let response; const authFactoryOutputs = []; - const browserClient = Client.fromBrowser({ + const baseClient = Client.from({ endpoint: 'https://cloud.appwrite.io/v1', projectId: 'auth-project', locale: 'en-US' }); - const browserHeaders = browserClient.getHeaders(); - authFactoryOutputs.push(browserHeaders['X-Appwrite-Project']); - authFactoryOutputs.push(browserHeaders['X-Appwrite-Locale']); - authFactoryOutputs.push(browserHeaders['x-sdk-platform']); + const baseHeaders = baseClient.getHeaders(); + authFactoryOutputs.push(baseHeaders['X-Appwrite-Project']); + authFactoryOutputs.push(baseHeaders['X-Appwrite-Locale']); + authFactoryOutputs.push(baseHeaders['x-sdk-platform']); const sessionClient = Client.fromSession({ endpoint: 'https://cloud.appwrite.io/v1', @@ -98,7 +98,7 @@ async function start() { } try { - Client.fromBrowser({ + Client.from({ endpoint: 'htp://cloud.appwrite.io/v1', projectId: 'auth-project' }); diff --git a/tests/languages/swift/Tests.swift b/tests/languages/swift/Tests.swift index 7a682a3688..f1a3384090 100644 --- a/tests/languages/swift/Tests.swift +++ b/tests/languages/swift/Tests.swift @@ -21,16 +21,16 @@ class Tests: XCTestCase { func test() async throws { do { var authFactoryOutputs: [String] = [] - let browserClient = try Client.fromBrowser( + let baseClient = try Client.from( endpoint: "https://cloud.appwrite.io/v1", projectId: "auth-project", locale: "en-US", selfSigned: true ) - let browserHeaders = browserClient.getHeaders() - authFactoryOutputs.append(browserHeaders["X-Appwrite-Project"] ?? "nil") - authFactoryOutputs.append(browserHeaders["X-Appwrite-Locale"] ?? "nil") - authFactoryOutputs.append(browserHeaders["x-sdk-platform"] ?? "nil") + let baseHeaders = baseClient.getHeaders() + authFactoryOutputs.append(baseHeaders["X-Appwrite-Project"] ?? "nil") + authFactoryOutputs.append(baseHeaders["X-Appwrite-Locale"] ?? "nil") + authFactoryOutputs.append(baseHeaders["x-sdk-platform"] ?? "nil") let sessionClient = try Client.fromSession( endpoint: "https://cloud.appwrite.io/v1", @@ -102,7 +102,7 @@ class Tests: XCTestCase { } do { - _ = try Client.fromBrowser( + _ = try Client.from( endpoint: "htp://cloud.appwrite.io/v1", projectId: "auth-project" ) diff --git a/tests/languages/web/index.html b/tests/languages/web/index.html index f119ca7a68..18abd4ddfc 100644 --- a/tests/languages/web/index.html +++ b/tests/languages/web/index.html @@ -40,18 +40,18 @@ // Ping console.log(`x-sdk-name: ${sdkHeaders['x-sdk-name']}; x-sdk-platform: ${sdkHeaders['x-sdk-platform']}; x-sdk-language: ${sdkHeaders['x-sdk-language']}; x-sdk-version: ${sdkHeaders['x-sdk-version']}`); - const browserClient = Client.fromBrowser({ + const baseClient = Client.from({ endpoint: 'https://cloud.appwrite.io/v1', projectId: 'auth-project', endpointRealtime: 'wss://realtime.example.com/v1', locale: 'en-US', selfSigned: true, }); - const browserHeaders = browserClient.getHeaders(); - console.log(browserHeaders['X-Appwrite-Project']); - console.log(browserHeaders['X-Appwrite-Locale']); - console.log(browserHeaders['x-sdk-platform']); - console.log(browserClient.config.endpointRealtime); + const baseHeaders = baseClient.getHeaders(); + console.log(baseHeaders['X-Appwrite-Project']); + console.log(baseHeaders['X-Appwrite-Locale']); + console.log(baseHeaders['x-sdk-platform']); + console.log(baseClient.config.endpointRealtime); const sessionClient = Client.fromSession({ endpoint: 'https://cloud.appwrite.io/v1', @@ -123,7 +123,7 @@ } try { - Client.fromBrowser({ + Client.from({ endpoint: 'htp://cloud.appwrite.io/v1', projectId: 'auth-project', }); @@ -132,7 +132,7 @@ } try { - Client.fromBrowser({ + Client.from({ endpoint: 'https://cloud.appwrite.io/v1', projectId: 'auth-project', endpointRealtime: 'ftp://cloud.appwrite.io/v1', diff --git a/tests/languages/web/node.js b/tests/languages/web/node.js index a80c5e3a63..3e8aa45b31 100644 --- a/tests/languages/web/node.js +++ b/tests/languages/web/node.js @@ -14,18 +14,18 @@ async function start() { const sdkHeaders = client.getHeaders(); console.log(`x-sdk-name: ${sdkHeaders['x-sdk-name']}; x-sdk-platform: ${sdkHeaders['x-sdk-platform']}; x-sdk-language: ${sdkHeaders['x-sdk-language']}; x-sdk-version: ${sdkHeaders['x-sdk-version']}`); - const browserClient = Client.fromBrowser({ + const baseClient = Client.from({ endpoint: 'https://cloud.appwrite.io/v1', projectId: 'auth-project', endpointRealtime: 'wss://realtime.example.com/v1', locale: 'en-US', selfSigned: true, }); - const browserHeaders = browserClient.getHeaders(); - console.log(browserHeaders['X-Appwrite-Project']); - console.log(browserHeaders['X-Appwrite-Locale']); - console.log(browserHeaders['x-sdk-platform']); - console.log(browserClient.config.endpointRealtime); + const baseHeaders = baseClient.getHeaders(); + console.log(baseHeaders['X-Appwrite-Project']); + console.log(baseHeaders['X-Appwrite-Locale']); + console.log(baseHeaders['x-sdk-platform']); + console.log(baseClient.config.endpointRealtime); const sessionClient = Client.fromSession({ endpoint: 'https://cloud.appwrite.io/v1', @@ -97,7 +97,7 @@ async function start() { } try { - Client.fromBrowser({ + Client.from({ endpoint: 'htp://cloud.appwrite.io/v1', projectId: 'auth-project', }); @@ -106,7 +106,7 @@ async function start() { } try { - Client.fromBrowser({ + Client.from({ endpoint: 'https://cloud.appwrite.io/v1', projectId: 'auth-project', endpointRealtime: 'ftp://cloud.appwrite.io/v1', From d4cddd5cac6d0a309dc2547c7b3c8f8dc3433071 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 11 May 2026 15:45:00 +0530 Subject: [PATCH 61/69] Store lowercase Flutter auth config aliases --- templates/flutter/lib/src/client.dart.twig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/templates/flutter/lib/src/client.dart.twig b/templates/flutter/lib/src/client.dart.twig index 61c772b321..c8a00eb9f3 100644 --- a/templates/flutter/lib/src/client.dart.twig +++ b/templates/flutter/lib/src/client.dart.twig @@ -133,6 +133,9 @@ abstract class Client implements ClientAuth { String value, ) { client.config[configKey] = value; + if (configKey != configKey.toLowerCase()) { + client.config[configKey.toLowerCase()] = value; + } client.addHeader(header, value); } From 8abc2ef04808ed16efbec19c585709972e01fdac Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 12 May 2026 16:12:32 +0530 Subject: [PATCH 62/69] refactor: address Android maintainer review comments - Inline fromClient into from; call from() directly in other factory methods - Remove callInternal wrapper; keep request logic in call() - Remove chunkedUploadInternal wrapper; keep upload logic in chunkedUpload() - Remove applySelfSigned wrapper; init calls setSelfSigned() directly - Omit explicit converter = null where parameter already defaults to null - Make Service.client a protected property so subclasses inherit it - Remove redundant private val client from Realtime and generated service constructors --- .../src/main/java/io/package/Client.kt.twig | 84 +++---------------- .../src/main/java/io/package/Service.kt.twig | 2 +- .../java/io/package/services/Realtime.kt.twig | 2 +- .../java/io/package/services/Service.kt.twig | 2 +- 4 files changed, 16 insertions(+), 74 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Client.kt.twig b/templates/android/library/src/main/java/io/package/Client.kt.twig index 1093cebcc5..a05c5a197e 100644 --- a/templates/android/library/src/main/java/io/package/Client.kt.twig +++ b/templates/android/library/src/main/java/io/package/Client.kt.twig @@ -61,22 +61,6 @@ class Client @JvmOverloads constructor( endpointRealtime: String? = null, locale: String? = null, selfSigned: Boolean = false - ): Client = fromClient( - context = context, - endpoint = endpoint, - projectId = projectId, - endpointRealtime = endpointRealtime, - locale = locale, - selfSigned = selfSigned - ) - - private fun fromClient( - context: Context, - endpoint: String, - projectId: String, - endpointRealtime: String?, - locale: String?, - selfSigned: Boolean ): Client { require(endpoint.startsWith("http://") || endpoint.startsWith("https://")) { "Invalid endpoint URL: $endpoint" @@ -138,10 +122,10 @@ class Client @JvmOverloads constructor( locale: String? = null, selfSigned: Boolean = false ): Client { - val client = fromClient( + val client = from( context = context, - endpoint = endpoint, projectId = projectId, + endpoint = endpoint, endpointRealtime = endpointRealtime, locale = locale, selfSigned = selfSigned @@ -167,10 +151,10 @@ class Client @JvmOverloads constructor( locale: String? = null, selfSigned: Boolean = false ): Client { - val client = fromClient( + val client = from( context = context, - endpoint = endpoint, projectId = projectId, + endpoint = endpoint, endpointRealtime = endpointRealtime, locale = locale, selfSigned = selfSigned @@ -204,10 +188,10 @@ class Client @JvmOverloads constructor( "Exactly one impersonation target must be provided" } - val client = fromClient( + val client = from( context = context, - endpoint = endpoint, projectId = projectId, + endpoint = endpoint, endpointRealtime = endpointRealtime, locale = locale, selfSigned = selfSigned @@ -288,7 +272,7 @@ class Client @JvmOverloads constructor( ) config = mutableMapOf() - applySelfSigned(selfSigned) + setSelfSigned(selfSigned) } {% for header in spec.global.headers %} @@ -323,11 +307,6 @@ class Client @JvmOverloads constructor( */ @Deprecated("Use Client.from or another factory method instead.") fun setSelfSigned(status: Boolean): Client { - applySelfSigned(status) - return this - } - - private fun applySelfSigned(status: Boolean) { selfSigned = status val builder = OkHttpClient() @@ -336,7 +315,7 @@ class Client @JvmOverloads constructor( if (!selfSigned) { http = builder.build() - return + return this } try { @@ -368,6 +347,8 @@ class Client @JvmOverloads constructor( } catch (e: Exception) { throw RuntimeException(e) } + + return this } /** @@ -458,8 +439,7 @@ class Client @JvmOverloads constructor( apiPath, apiHeaders, apiParams, - responseType = String::class.java, - converter = null + responseType = String::class.java ) } @@ -481,22 +461,6 @@ class Client @JvmOverloads constructor( params: Map = mapOf(), responseType: Class, converter: ((Any) -> T)? = null - ): T = callInternal( - method = method, - path = path, - headers = headers, - params = params, - responseType = responseType, - converter = converter - ) - - suspend fun callInternal( - method: String, - path: String, - headers: Map, - params: Map, - responseType: Class, - converter: ((Any) -> T)? ): T { val filteredParams = params.filterValues { it != null } @@ -592,26 +556,6 @@ class Client @JvmOverloads constructor( paramName: String, idParamName: String? = null, onProgress: ((UploadProgress) -> Unit)? = null, - ): T = chunkedUploadInternal( - path = path, - headers = headers, - params = params, - responseType = responseType, - converter = converter, - paramName = paramName, - idParamName = idParamName, - onProgress = onProgress - ) - - suspend fun chunkedUploadInternal( - path: String, - headers: MutableMap, - params: MutableMap, - responseType: Class, - converter: ((Any) -> T), - paramName: String, - idParamName: String?, - onProgress: ((UploadProgress) -> Unit)?, ): T { var file: RandomAccessFile? = null val input = params[paramName] as InputFile @@ -658,8 +602,7 @@ class Client @JvmOverloads constructor( path = "$path/${params[idParamName]}", headers = headers, params = emptyMap(), - responseType = Map::class.java, - converter = null + responseType = Map::class.java ) val chunksUploaded = current["chunksUploaded"] as Long offset = chunksUploaded * CHUNK_SIZE @@ -700,8 +643,7 @@ class Client @JvmOverloads constructor( path, headers, params, - responseType = Map::class.java, - converter = null + responseType = Map::class.java ) offset += CHUNK_SIZE diff --git a/templates/android/library/src/main/java/io/package/Service.kt.twig b/templates/android/library/src/main/java/io/package/Service.kt.twig index 47f4dec8bc..d3633bba01 100644 --- a/templates/android/library/src/main/java/io/package/Service.kt.twig +++ b/templates/android/library/src/main/java/io/package/Service.kt.twig @@ -7,4 +7,4 @@ import {{ sdk.namespace | caseDot }}.Client * * @param client The Appwrite client. */ -abstract class Service(@Suppress("UNUSED_PARAMETER") client: Client) +abstract class Service(protected val client: Client) diff --git a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig index 5bde51d234..e77180fea6 100644 --- a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig @@ -25,7 +25,7 @@ import java.util.concurrent.atomic.AtomicInteger import android.util.Log import kotlin.coroutines.CoroutineContext -class Realtime(private val client: Client) : Service(client), CoroutineScope { +class Realtime(client: Client) : Service(client), CoroutineScope { private val job = Job() diff --git a/templates/android/library/src/main/java/io/package/services/Service.kt.twig b/templates/android/library/src/main/java/io/package/services/Service.kt.twig index 294f1ef6cc..079ef06e37 100644 --- a/templates/android/library/src/main/java/io/package/services/Service.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/Service.kt.twig @@ -25,7 +25,7 @@ import java.io.File /** * {{ service.description | replace({"\n": "\n * "}) | raw }} */ -class {{ service.name | caseUcfirst }}(private val client: Client) : Service(client) { +class {{ service.name | caseUcfirst }}(client: Client) : Service(client) { {% for method in service.methods %} /** From 07460677d979fd5889402fc36afa2dc140ac7637 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 13 May 2026 10:44:21 +0530 Subject: [PATCH 63/69] Fix Swift CI auth factory tests --- templates/apple/base/params.twig | 2 +- tests/languages/apple/Tests.swift | 12 ++++++------ tests/languages/swift/Tests.swift | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/templates/apple/base/params.twig b/templates/apple/base/params.twig index 5ca153e560..93af1b1d5e 100644 --- a/templates/apple/base/params.twig +++ b/templates/apple/base/params.twig @@ -10,7 +10,7 @@ {%- else -%} let {%- endif %} apiParams: [String: Any?] = [ {%~ for parameter in method.parameters.query | merge(method.parameters.body) %} - "{{ parameter.name }}": {{ parameter.name | caseCamel | escapeSwiftKeyword }}{% if not loop.last or (method.type == 'location' or method.type == 'webAuth' and method.auth | length > 0) %},{% endif %} + "{{ parameter.name }}": {{ parameter.name | caseCamel | escapeSwiftKeyword }}{% if parameter.enumValues is not empty %}{% if parameter.type == 'array' %}{% if not parameter.required %}?{% endif %}.map { $0.rawValue }{% else %}{% if not parameter.required %}?{% endif %}.rawValue{% endif %}{% endif %}{% if not loop.last or (method.type == 'location' or method.type == 'webAuth' and method.auth | length > 0) %},{% endif %} {%~ endfor %} {%~ if method.type == 'location' or method.type == 'webAuth' %} diff --git a/tests/languages/apple/Tests.swift b/tests/languages/apple/Tests.swift index 793bba3634..5c378859d7 100644 --- a/tests/languages/apple/Tests.swift +++ b/tests/languages/apple/Tests.swift @@ -82,8 +82,8 @@ class Tests: XCTestCase { projectId: "auth-project", session: "auth-session" ) - } catch { - print(error.localizedDescription) + } catch let error as AppwriteError { + print(error.message) } do { @@ -91,8 +91,8 @@ class Tests: XCTestCase { endpoint: "htp://cloud.appwrite.io/v1", projectId: "auth-project" ) - } catch { - print(error.localizedDescription) + } catch let error as AppwriteError { + print(error.message) } do { @@ -101,8 +101,8 @@ class Tests: XCTestCase { projectId: "auth-project", endpointRealtime: "ftp://cloud.appwrite.io/v1" ) - } catch { - print(error.localizedDescription) + } catch let error as AppwriteError { + print(error.message) } // Ping pong test diff --git a/tests/languages/swift/Tests.swift b/tests/languages/swift/Tests.swift index f1a3384090..f5e29ff86b 100644 --- a/tests/languages/swift/Tests.swift +++ b/tests/languages/swift/Tests.swift @@ -97,8 +97,8 @@ class Tests: XCTestCase { projectId: "auth-project", session: "auth-session" ) - } catch { - authFactoryOutputs.append(error.localizedDescription) + } catch let error as AppwriteError { + authFactoryOutputs.append(error.message) } do { @@ -106,8 +106,8 @@ class Tests: XCTestCase { endpoint: "htp://cloud.appwrite.io/v1", projectId: "auth-project" ) - } catch { - authFactoryOutputs.append(error.localizedDescription) + } catch let error as AppwriteError { + authFactoryOutputs.append(error.message) } let client = Client() From 7a55bf3bcc2a6c700e5c5febf14be1b143b75590 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 14 May 2026 13:20:07 +0530 Subject: [PATCH 64/69] Remove accidental Android Realtime conflict changes --- .../java/io/package/services/Realtime.kt.twig | 25 +++---------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig index 0861b691cf..04fbda122e 100644 --- a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig @@ -17,9 +17,6 @@ import okhttp3.Request import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener -import okhttp3.internal.concurrent.TaskRunner -import okhttp3.internal.ws.RealWebSocket -import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicInteger import android.util.Log @@ -40,7 +37,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { private const val TYPE_RESPONSE = "response" private const val HEARTBEAT_INTERVAL = 20_000L // 20 seconds - private var socket: RealWebSocket? = null + private var socket: WebSocket? = null private val activeSubscriptions = ConcurrentHashMap() private val pendingSubscribes = LinkedHashMap>() @@ -55,8 +52,6 @@ class Realtime(client: Client) : Service(client), CoroutineScope { private fun createSocket() { // Serialize socket recreation so subscribe(A), subscribe(B), subscribe(C) // deterministically results in A -> A+B -> A+B+C (sequential updates). - val request: Request - val newSocket: RealWebSocket? synchronized(subscriptionLock) { if (activeSubscriptions.isEmpty()) { reconnect = false @@ -67,7 +62,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { val encodedProject = java.net.URLEncoder.encode(client.config["project"].toString(), "UTF-8") val queryParams = "project=$encodedProject" val url = "${client.endpointRealtime}/realtime?$queryParams" - request = Request.Builder().url(url).build() + val request = Request.Builder().url(url).build() if (socket != null) { reconnect = false @@ -77,20 +72,8 @@ class Realtime(client: Client) : Service(client), CoroutineScope { } val generation = socketGeneration.incrementAndGet() - newSocket = RealWebSocket( - taskRunner = TaskRunner.INSTANCE, - originalRequest = request, - listener = {{ spec.title | caseUcfirst }}WebSocketListener(generation), - random = Random(), - pingIntervalMillis = client.getHttpClient().pingIntervalMillis.toLong(), - extensions = null, - minimumDeflateSize = client.getHttpClient().minWebSocketMessageToCompress, - webSocketCloseTimeout = client.getHttpClient().webSocketCloseTimeout.toLong() - ) - socket = newSocket + socket = client.http.newWebSocket(request, {{ spec.title | caseUcfirst }}WebSocketListener(generation)) } - - newSocket?.connect(client.getHttpClient()) } private fun closeSocket() { @@ -423,4 +406,4 @@ class Realtime(client: Client) : Service(client), CoroutineScope { t.printStackTrace() } } -} +} \ No newline at end of file From 5c1973dad34bf15fccf4e4c653465ccc9e7a3bda Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 14 May 2026 13:25:04 +0530 Subject: [PATCH 65/69] Limit React Native auth factories to client flows --- templates/react-native/src/client.ts.twig | 7 ------- 1 file changed, 7 deletions(-) diff --git a/templates/react-native/src/client.ts.twig b/templates/react-native/src/client.ts.twig index e9ad82cdf5..84b1b6f5b4 100644 --- a/templates/react-native/src/client.ts.twig +++ b/templates/react-native/src/client.ts.twig @@ -205,13 +205,6 @@ class Client { .setSession(params.session); } - static fromCookie(params: BaseClientParams & { cookie: string }): Client { - const client = new Client().applyBase(params); - client.config.cookie = params.cookie; - client.headers['Cookie'] = params.cookie; - return client; - } - static fromDevKey(params: BaseClientParams & { devKey: string }): Client { const client = new Client().applyBase(params); client.config.devkey = params.devKey; From 68cb6fca3734462eafdc5f548080bbdea8e200d5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 15 May 2026 14:51:11 +0530 Subject: [PATCH 66/69] Fix web examples session auth --- templates/web/docs/example.md.twig | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/templates/web/docs/example.md.twig b/templates/web/docs/example.md.twig index 718e858210..430a03f23e 100644 --- a/templates/web/docs/example.md.twig +++ b/templates/web/docs/example.md.twig @@ -5,7 +5,13 @@ const client = new Client() {%~ if method.auth|length > 0 %} .setEndpoint('{{ spec.endpointDocs | raw }}') // Your API Endpoint {%~ for node in method.auth %} - {%~ for key,header in node|keys %} + {%~ set authHeaders = [] %} + {%~ for header in node|keys %} + {%~ if not (header == 'Session' and 'client' in method.platforms) %} + {%~ set authHeaders = authHeaders|merge([header]) %} + {%~ endif %} + {%~ endfor %} + {%~ for header in authHeaders %} .set{{header}}('{{node[header]['x-appwrite']['demo'] | raw }}'){% if loop.last %};{% endif%} // {{node[header].description}} {%~ endfor %} {%~ endfor %} From 1915d0c58fc2342150a1328c702d311dfc79d861 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 19 May 2026 16:05:58 +0530 Subject: [PATCH 67/69] Update docs examples for auth factories --- templates/android/docs/java/example.md.twig | 41 ++++++++++++---- templates/android/docs/kotlin/example.md.twig | 33 +++++++++---- templates/dart/docs/example.md.twig | 29 +++++++---- templates/flutter/docs/example.md.twig | 34 +++++++++---- templates/kotlin/docs/java/example.md.twig | 37 +++++++++++--- templates/kotlin/docs/kotlin/example.md.twig | 25 +++++++--- templates/node/docs/example.md.twig | 29 +++++++---- templates/react-native/docs/example.md.twig | 42 +++++++++------- templates/swift/docs/example.md.twig | 35 +++++++++----- templates/web/docs/example.md.twig | 48 ++++++++++--------- 10 files changed, 246 insertions(+), 107 deletions(-) diff --git a/templates/android/docs/java/example.md.twig b/templates/android/docs/java/example.md.twig index fb7a9909f4..40511037a1 100644 --- a/templates/android/docs/java/example.md.twig +++ b/templates/android/docs/java/example.md.twig @@ -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); @@ -56,6 +78,5 @@ Client client = new Client(context) }) ); {% endif %} - {% endfor %} ``` diff --git a/templates/android/docs/kotlin/example.md.twig b/templates/android/docs/kotlin/example.md.twig index 74a0beb9d6..0043763ecb 100644 --- a/templates/android/docs/kotlin/example.md.twig +++ b/templates/android/docs/kotlin/example.md.twig @@ -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) diff --git a/templates/dart/docs/example.md.twig b/templates/dart/docs/example.md.twig index 12c6ba360d..0f16c47d8a 100644 --- a/templates/dart/docs/example.md.twig +++ b/templates/dart/docs/example.md.twig @@ -17,15 +17,26 @@ import 'package:{{ language.params.packageName }}/permission.dart'; import 'package:{{ language.params.packageName }}/role.dart'; {% endif %} -Client client = Client() - {%~ 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}}('{{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 = authNode|keys %} +{% set factory = 'from' %} +{% if 'ImpersonateUserId' in authHeaders or 'ImpersonateUserEmail' in authHeaders or 'ImpersonateUserPhone' in authHeaders %}{% set factory = 'fromImpersonation' %}{% elseif 'Key' in authHeaders %}{% set factory = 'fromAPIKey' %}{% elseif 'Cookie' in authHeaders %}{% set factory = 'fromCookie' %}{% elseif 'JWT' in authHeaders %}{% set factory = 'fromJWT' %}{% elseif 'DevKey' in authHeaders %}{% set factory = 'fromDevKey' %}{% elseif 'Session' in authHeaders %}{% set factory = 'fromSession' %}{% endif %} +{% if method.auth|length > 0 %} +Client client = Client.{{ factory }}( + endPoint: '{{ spec.endpointDocs | raw }}', // Your API Endpoint + projectId: '{{ authNode.Project['x-appwrite']['demo'] | raw }}', // Your project ID +{% for header in authHeaders %} +{% if header != 'Project' %} + {% if header == 'Key' %}apiKey{% elseif header == 'JWT' %}jwt{% elseif header == 'Cookie' %}cookie{% elseif 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 }}', // {{ authNode[header].description }} +{% endif %} +{% endfor %} +); +{% else %} +Client client = Client(); +{% endif %} {{ service.name | caseUcfirst }} {{ service.name | caseCamel }} = {{service.name | caseUcfirst}}(client); diff --git a/templates/flutter/docs/example.md.twig b/templates/flutter/docs/example.md.twig index 192ab4d475..17a0cab265 100644 --- a/templates/flutter/docs/example.md.twig +++ b/templates/flutter/docs/example.md.twig @@ -17,15 +17,31 @@ import 'package:{{ language.params.packageName }}/permission.dart'; import 'package:{{ language.params.packageName }}/role.dart'; {% endif %} -Client client = Client() - {%~ 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}}('{{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 %} +ClientAuth client = Client.{{ factory }}( + endPoint: '{{ spec.endpointDocs | raw }}', // Your API Endpoint + projectId: '{{ authNode.Project['x-appwrite']['demo'] | raw }}', // 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 }}', // {{ authNode[header].description }} +{% endif %} +{% endfor %} +); +{% else %} +Client client = Client(); +{% endif %} {{ service.name | caseUcfirst }} {{ service.name | caseCamel }} = {{service.name | caseUcfirst}}(client); diff --git a/templates/kotlin/docs/java/example.md.twig b/templates/kotlin/docs/java/example.md.twig index 362df5be7d..6e9aefb65f 100644 --- a/templates/kotlin/docs/java/example.md.twig +++ b/templates/kotlin/docs/java/example.md.twig @@ -19,13 +19,37 @@ import {{ sdk.namespace | caseDot }}.enums.{{ parameter.enumName | caseUcfirst } {% endif %} {% endfor %} -Client client = new Client() -{% if method.auth|length > 0 %} - .setEndpoint("{{ spec.endpointDocs | raw }}") // Your API Endpoint +{% set authNode = {} %} {% 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 = authNode|merge(node) %} +{% endfor %} +{% set authHeaders = authNode|keys %} +{% set factory = 'from' %} +{% if 'ImpersonateUserId' in authHeaders or 'ImpersonateUserEmail' in authHeaders or 'ImpersonateUserPhone' in authHeaders %}{% set factory = 'fromImpersonation' %}{% elseif 'Key' in authHeaders %}{% set factory = 'fromAPIKey' %}{% elseif 'Cookie' in authHeaders %}{% set factory = 'fromCookie' %}{% elseif 'JWT' in authHeaders %}{% set factory = 'fromJWT' %}{% elseif 'DevKey' in authHeaders %}{% set factory = 'fromDevKey' %}{% elseif 'Session' in authHeaders %}{% set factory = 'fromSession' %}{% endif %} +{% if method.auth|length > 0 %} +Client client = Client.{{ factory }}( + "{{ 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 == 'fromAPIKey' %} + "{{ authNode.Key['x-appwrite']['demo'] | raw }}", // {{ authNode.Key.description }} +{% elseif factory == 'fromCookie' %} + "{{ authNode.Cookie['x-appwrite']['demo'] | raw }}", // {{ authNode.Cookie.description }} +{% elseif factory == 'fromJWT' %} + "{{ authNode.JWT['x-appwrite']['demo'] | raw }}", // {{ authNode.JWT.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(); +{% endif %} {{ service.name | caseUcfirst }} {{ service.name | caseCamel }} = new {{ service.name | caseUcfirst }}(client); @@ -52,6 +76,5 @@ Client client = new Client() }) ); {% endif %} - {% endfor %} ``` diff --git a/templates/kotlin/docs/kotlin/example.md.twig b/templates/kotlin/docs/kotlin/example.md.twig index e20156e7a5..075bc328b2 100644 --- a/templates/kotlin/docs/kotlin/example.md.twig +++ b/templates/kotlin/docs/kotlin/example.md.twig @@ -19,13 +19,26 @@ import {{ sdk.namespace | caseDot }}.Permission import {{ sdk.namespace | caseDot }}.Role {% endif %} -val client = Client() -{% if method.auth|length > 0 %} - .setEndpoint("{{ spec.endpointDocs | raw }}") // Your API Endpoint +{% set authNode = {} %} {% 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 %} +{% set authNode = authNode|merge(node) %} +{% endfor %} +{% set authHeaders = authNode|keys %} +{% set factory = 'from' %} +{% if 'ImpersonateUserId' in authHeaders or 'ImpersonateUserEmail' in authHeaders or 'ImpersonateUserPhone' in authHeaders %}{% set factory = 'fromImpersonation' %}{% elseif 'Key' in authHeaders %}{% set factory = 'fromAPIKey' %}{% elseif 'Cookie' in authHeaders %}{% set factory = 'fromCookie' %}{% elseif 'JWT' in authHeaders %}{% set factory = 'fromJWT' %}{% elseif 'DevKey' in authHeaders %}{% set factory = 'fromDevKey' %}{% elseif 'Session' in authHeaders %}{% set factory = 'fromSession' %}{% endif %} +{% if method.auth|length > 0 %} +val client = Client.{{ factory }}( + 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 == 'Key' %}apiKey{% elseif header == 'JWT' %}jwt{% elseif header == 'Cookie' %}cookie{% elseif 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() +{% endif %} val {{ service.name | caseCamel }} = {{ service.name | caseUcfirst }}(client) diff --git a/templates/node/docs/example.md.twig b/templates/node/docs/example.md.twig index ef482eca1c..bbd91993f1 100644 --- a/templates/node/docs/example.md.twig +++ b/templates/node/docs/example.md.twig @@ -4,15 +4,26 @@ const sdk = require('node-{{ spec.title | caseDash }}'); const fs = require('fs'); {% endif %} -const client = new sdk.Client() - {%~ 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}}('{{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 = authNode|keys %} +{% set factory = 'from' %} +{% if 'ImpersonateUserId' in authHeaders or 'ImpersonateUserEmail' in authHeaders or 'ImpersonateUserPhone' in authHeaders %}{% set factory = 'fromImpersonation' %}{% elseif 'Key' in authHeaders %}{% set factory = 'fromAPIKey' %}{% elseif 'Cookie' in authHeaders %}{% set factory = 'fromCookie' %}{% elseif 'JWT' in authHeaders %}{% set factory = 'fromJWT' %}{% elseif 'DevKey' in authHeaders %}{% set factory = 'fromDevKey' %}{% elseif 'Session' in authHeaders %}{% set factory = 'fromSession' %}{% endif %} +{% if method.auth|length > 0 %} +const client = sdk.Client.{{ factory }}({ + 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 == 'Key' %}apiKey{% elseif header == 'JWT' %}jwt{% elseif header == 'Cookie' %}cookie{% elseif header == 'Session' %}session{% elseif header == 'DevKey' %}devKey{% elseif header == 'ImpersonateUserId' %}userId{% elseif header == 'ImpersonateUserEmail' %}email{% elseif header == 'ImpersonateUserPhone' %}phone{% else %}{{ header | caseCamel }}{% endif %}: '{{ authNode[header]['x-appwrite']['demo'] | raw }}'{% if not loop.last %},{% endif %} // {{ authNode[header].description }} +{% endif %} +{% endfor %} +}); +{% else %} +const client = new sdk.Client(); +{% endif %} const {{ service.name | caseCamel | escapeKeyword }} = new sdk.{{service.name | caseUcfirst}}(client); diff --git a/templates/react-native/docs/example.md.twig b/templates/react-native/docs/example.md.twig index 1ae748b7b8..e0572a466d 100644 --- a/templates/react-native/docs/example.md.twig +++ b/templates/react-native/docs/example.md.twig @@ -1,15 +1,31 @@ ```javascript import { Client, {{ service.name | caseUcfirst }}{% for parameter in method.parameters.all %}{% if parameter.enumValues | length > 0%}, {{ parameter.enumName | caseUcfirst}}{% endif %}{% endfor %}{% if method.parameters.all | hasPermissionParam %}, Permission, Role{% endif %} } from "{{ language.params.npmPackage }}"; -const client = new Client() - {%~ 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}}('{{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 %} +const client = Client.{{ factory }}({ + 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' %}email{% elseif header == 'ImpersonateUserPhone' %}phone{% else %}{{ header | caseCamel }}{% endif %}: '{{ authNode[header]['x-appwrite']['demo'] | raw }}'{% if not loop.last %},{% endif %} // {{ authNode[header].description }} +{% endif %} +{% endfor %} +}); +{% else %} +const client = new Client(); +{% endif %} const {{ service.name | caseCamel }} = new {{service.name | caseUcfirst}}(client{% if service.globalParams | length %}{% for parameter in service.globalParams %}, {{ parameter | paramExample }}{% endfor %}{% endif %}); @@ -23,10 +39,4 @@ const {{ service.name | caseCamel }} = new {{service.name | caseUcfirst}}(client {{ parameter.name | caseCamel | escapeKeyword }}: {% if parameter.enumValues | length > 0 %}{{ parameter | enumExample }}{% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %} // optional {%~ endif %} {%~ endfor -%} -}); -{% endif %} - -{% if method.type != 'webAuth' %} -console.log(result); -{% endif %} -``` +});{%- endif -%}{% if method.type != 'webAuth' %}{{ "\n\nconsole.log(result);\n```" }}{% else %}{{ "\n```" }}{% endif %} diff --git a/templates/swift/docs/example.md.twig b/templates/swift/docs/example.md.twig index d381b1339c..c27d1c47ee 100644 --- a/templates/swift/docs/example.md.twig +++ b/templates/swift/docs/example.md.twig @@ -8,26 +8,39 @@ import {{ spec.title | caseUcfirst }}Enums {% endif %} {% endfor %} -let client = Client() -{% if method.auth|length > 0 %} - .setEndpoint("{{ spec.endpointDocs | raw }}") // Your API Endpoint +{% set authNode = {} %} {% for node in method.auth %} -{% for key,header in node|keys %} - .set{{header | caseUcfirst}}("{{node[header]['x-appwrite']['demo'] | raw }}") // {{node[header].description}} +{% 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 'Key' in authHeaders %}{% set factory = 'fromAPIKey' %}{% elseif 'Cookie' in authHeaders %}{% set factory = 'fromCookie' %}{% elseif 'JWT' in authHeaders %}{% set factory = 'fromJWT' %}{% elseif 'DevKey' in authHeaders %}{% set factory = 'fromDevKey' %}{% elseif 'Session' in authHeaders %}{% set factory = 'fromSession' %}{% endif %} +{% if method.auth|length > 0 %} +let client = try Client.{{ factory }}( + 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 == 'Key' %}apiKey{% elseif header == 'JWT' %}jwt{% elseif header == 'Cookie' %}cookie{% elseif 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 %} +let client = Client() {% endif %} let {{ service.name | caseCamel }} = {{ service.name | caseUcfirst }}(client{% if service.globalParams | length %}{% for parameter in service.globalParams %}, {{ parameter | paramExample }}{% endfor %}{% endif %}) -let {% if method.type == 'webAuth' %}success{% elseif method.type == 'location' %}bytes{% elseif (method | getValidResponseModels)|length > 1 or method.responseModel | length == 0 %}result{% else %}{{ method.responseModel | caseCamel | escapeSwiftKeyword }}{% endif %} = try await {{ service.name | caseCamel }}.{{ method.name | caseCamel }}({% if method.parameters.all | length == 0 %}){{ '\n' }}{% endif %} +let {% if method.type == 'webAuth' %}success{% elseif method.type == 'location' %}bytes{% elseif (method | getValidResponseModels)|length > 1 or method.responseModel | length == 0 %}result{% else %}{{ method.responseModel | caseCamel | escapeSwiftKeyword }}{% endif %} = try await {{ service.name | caseCamel }}.{{ method.name | caseCamel }}({% if method.parameters.all | length == 0 %}){% else %} - {%~ for parameter in method.parameters.all %} +{% for parameter in method.parameters.all %} {{ parameter.name }}: {% if parameter.enumValues | length > 0 %}{{ parameter | enumExample }}{% else %}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %}{% if not parameter.required %} // optional{% endif %} - {%~ if loop.last %} - +{% endfor %} ) {% endif %} - -{% endfor %} ``` diff --git a/templates/web/docs/example.md.twig b/templates/web/docs/example.md.twig index 430a03f23e..bfb62715f2 100644 --- a/templates/web/docs/example.md.twig +++ b/templates/web/docs/example.md.twig @@ -1,21 +1,31 @@ ```javascript import { Client, {{ service.name | caseUcfirst }}{% for parameter in method.parameters.all %}{% if parameter.enumValues | length > 0%}, {{ parameter.enumName | caseUcfirst}}{% endif %}{% endfor %}{% if method.parameters.all | hasPermissionParam %}, Permission, Role{% endif %} } from "{{ language.params.npmPackage }}"; -const client = new Client() - {%~ if method.auth|length > 0 %} - .setEndpoint('{{ spec.endpointDocs | raw }}') // Your API Endpoint - {%~ for node in method.auth %} - {%~ set authHeaders = [] %} - {%~ for header in node|keys %} - {%~ if not (header == 'Session' and 'client' in method.platforms) %} - {%~ set authHeaders = authHeaders|merge([header]) %} - {%~ endif %} - {%~ endfor %} - {%~ for header in authHeaders %} - .set{{header}}('{{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 'Key' in authHeaders %}{% set factory = 'fromAPIKey' %}{% elseif 'Cookie' in authHeaders %}{% set factory = 'fromCookie' %}{% elseif 'JWT' in authHeaders %}{% set factory = 'fromJWT' %}{% elseif 'DevKey' in authHeaders %}{% set factory = 'fromDevKey' %}{% elseif 'Session' in authHeaders %}{% set factory = 'fromSession' %}{% endif %} +{% if method.auth|length > 0 %} +const client = Client.{{ factory }}({ + 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 == 'Key' %}apiKey{% elseif header == 'JWT' %}jwt{% elseif header == 'Cookie' %}cookie{% elseif header == 'Session' %}session{% elseif header == 'DevKey' %}devKey{% elseif header == 'ImpersonateUserId' %}userId{% elseif header == 'ImpersonateUserEmail' %}email{% elseif header == 'ImpersonateUserPhone' %}phone{% else %}{{ header | caseCamel }}{% endif %}: '{{ authNode[header]['x-appwrite']['demo'] | raw }}'{% if not loop.last %},{% endif %} // {{ authNode[header].description }} +{% endif %} +{% endfor %} +}); +{% else %} +const client = new Client(); +{% endif %} const {{ service.name | caseCamel | escapeKeyword }} = new {{service.name | caseUcfirst}}(client{% if service.globalParams | length %}{% for parameter in service.globalParams %}, {{ parameter | paramExample }}{% endfor %}{% endif %}); @@ -29,10 +39,4 @@ const {{ service.name | caseCamel | escapeKeyword }} = new {{service.name | case {{ parameter.name | caseCamel | escapeKeyword }}: {% if parameter.enumValues | length > 0 %}{{ parameter | enumExample }}{% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %} // optional {%~ endif %} {%~ endfor -%} -}); -{% endif %} - -{% if method.type != 'webAuth' %} -console.log(result); -{% endif %} -``` +});{%- endif -%}{% if method.type != 'webAuth' %}{{ "\n\nconsole.log(result);\n```" }}{% else %}{{ "\n```" }}{% endif %} From 624439c68f306350f0c8c64a7a7ed83c513c867b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 20 May 2026 15:45:50 +0530 Subject: [PATCH 68/69] fix(web): default Client to client platform and stop leaking auth into URLs - Fix bare new Client() defaulting to server platform; default to 'client' so browser consumers keep credentials: 'include', X-Fallback-Cookies, and correct x-sdk-platform header. - Stop injecting client.config auth values (session, key, jwt, cookie, devkey) into location/webAuth URL query strings. - For server-only location methods (e.g. getDeploymentDownload), match Node SDK behavior by returning Promise via authenticated client.call() instead of returning a credential-bearing URL string. - Client/mixed location methods (avatars, storage previews) still return URL strings but without auth credentials in query params. --- src/SDK/Language/Web.php | 4 ++++ templates/web/src/client.ts.twig | 4 ++-- templates/web/src/services/template.ts.twig | 18 ++++++++++-------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index ba83ebd7aa..2003d86b05 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -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'; + } return 'string'; } diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 1484bb1f82..8b21c3378b 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -407,14 +407,14 @@ class ClientRuntime { selfSigned: false, }; - private sdkPlatform: SDKPlatform = '{{ sdk.platform == 'server' ? 'server' : 'client' }}'; + private sdkPlatform: SDKPlatform = 'client'; /** * Custom headers for API requests. */ headers: Headers = { 'x-sdk-name': '{{ sdk.name }}', - 'x-sdk-platform': '{{ sdk.platform }}', + 'x-sdk-platform': 'client', 'x-sdk-language': '{{ language.name | caseLower }}', 'x-sdk-version': '{{ sdk.version }}', {%~ for key,header in spec.global.defaultHeaders %} diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index 9ead9af831..1f063d9438 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -168,14 +168,6 @@ class {{ service.name | caseUcfirst }}Runtime { } {%~ if method.type == 'location' or method.type == 'webAuth' %} - {%~ if method.auth|length > 0 %} - {%~ for node in method.auth %} - {%~ for key,header in node|keys %} - payload['{{header|caseLower}}'] = (this.client.config as unknown as Record)['{{header|caseLower}}']; - {%~ endfor %} - {%~ endfor %} - {%~ endif %} - for (const [key, value] of Object.entries(Service.flatten(payload))) { uri.searchParams.append(key, value); } @@ -189,7 +181,17 @@ class {{ service.name | caseUcfirst }}Runtime { return uri.toString(); } {%~ elseif method.type == 'location' %} + {%~ if ('server' in method.platforms or 'console' in method.platforms) and not ('client' in method.platforms) %} + return this.client.call( + '{{ method.method | caseLower }}', + uri, + apiHeaders, + payload, + 'arrayBuffer' + ); + {%~ else %} return uri.toString(); + {%~ endif %} {%~ elseif 'multipart/form-data' in method.consumes %} return this.client.chunkedUpload( '{{ method.method | caseLower }}', From dda722d11534f99acf57a1b9cbd009d99ee4af3f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 20 May 2026 17:18:17 +0530 Subject: [PATCH 69/69] fix(web): restore project ID to URL builders while keeping secrets out Re-add safe config headers (project, locale, mode, platform) to location/webAuth URL query strings so that and other header-less contexts remain self-contained. Credential-like headers (session, key, jwt, cookie, devkey, impersonation) are still excluded. Server-only location methods (e.g. getDeploymentDownload) continue to return Promise via authenticated client.call() and do not inject project into the URL, matching sdk-for-node behavior. --- templates/web/src/services/template.ts.twig | 22 +++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index 1f063d9438..ad2c72db8e 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -167,13 +167,17 @@ class {{ service.name | caseUcfirst }}Runtime { {%~ endfor %} } - {%~ if method.type == 'location' or method.type == 'webAuth' %} + {%~ if method.type == 'webAuth' %} + {%~ for node in method.auth %} + {%~ for key,header in node|keys %} + {%~ if header|caseLower in ['project', 'locale', 'mode', 'platform'] %} + payload['{{header|caseLower}}'] = (this.client.config as unknown as Record)['{{header|caseLower}}']; + {%~ endif %} + {%~ endfor %} + {%~ endfor %} for (const [key, value] of Object.entries(Service.flatten(payload))) { uri.searchParams.append(key, value); } - - {%~ endif %} - {%~ if method.type == 'webAuth' %} if (typeof window !== 'undefined' && window?.location) { window.location.href = uri.toString(); return; @@ -190,6 +194,16 @@ class {{ service.name | caseUcfirst }}Runtime { 'arrayBuffer' ); {%~ else %} + {%~ for node in method.auth %} + {%~ for key,header in node|keys %} + {%~ if header|caseLower in ['project', 'locale', 'mode', 'platform'] %} + payload['{{header|caseLower}}'] = (this.client.config as unknown as Record)['{{header|caseLower}}']; + {%~ endif %} + {%~ endfor %} + {%~ endfor %} + for (const [key, value] of Object.entries(Service.flatten(payload))) { + uri.searchParams.append(key, value); + } return uri.toString(); {%~ endif %} {%~ elseif 'multipart/form-data' in method.consumes %}