From 9d576307d8e1745f0032782fd6977c2ad18700bb Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 27 Apr 2026 16:40:57 +0530 Subject: [PATCH 01/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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",