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

Filter by extension

Filter by extension


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

## 0.25.0
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Version was bumped here but not in package.json 🤔


* [BREAKING] Changed `$sequence` type from `number` to `string` for `Row` and `Document` models
* Added impersonation support: `setImpersonateUserId()`, `setImpersonateUserEmail()`, `setImpersonateUserPhone()` on `Client`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Document impersonation header persistence to prevent cross-request leakage.

Given Client stores impersonation headers on this.headers (src/client.ts:314-360) and merges them into every call (src/client.ts:576-625), impersonation can carry over on shared client instances. Please note this behavior in changelog (or add explicit clear methods and document them).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` at line 6, Update the changelog entry to document that
impersonation headers set via Client.setImpersonateUserId,
Client.setImpersonateUserEmail, and Client.setImpersonateUserPhone are stored on
the Client instance (this.headers) and are merged into every request (the merge
step used in request/perform call paths), which means impersonation can persist
across requests on a shared Client; either note this persistence and recommend
creating separate Client instances per user or add and document explicit
clearing methods (e.g., clearImpersonation or removeImpersonateUser*) on Client
to avoid cross-request leakage.

* Added `impersonator` and `impersonatorUserId` optional fields to `User` model
* Added custom `toString()` on response data objects using `JSONbig.stringify` to fix BigInt serialization
* Updated `Log` model field descriptions to clarify impersonation behavior for `userId`, `userEmail`, `userName`
* Updated `X-Appwrite-Response-Format` header to `1.9.0`
* Updated devDependencies: Rollup 2→3, TypeScript pinned to 5.7.3, and related plugin upgrades

## 0.24.1

* Fix very large double values (for example 1.7976931348623157e+308) from being expanded into giant integer literals.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Appwrite React Native SDK

![License](https://img.shields.io/github/license/appwrite/sdk-for-react-native.svg?style=flat-square)
![Version](https://img.shields.io/badge/api%20version-1.8.1-blue.svg?style=flat-square)
![Version](https://img.shields.io/badge/api%20version-1.9.0-blue.svg?style=flat-square)
[![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator)
[![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite)
[![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord)

**This SDK is compatible with Appwrite server version latest. For older versions, please check [previous releases](https://github.com/appwrite/sdk-for-react-native/releases).**
**This SDK is compatible with Appwrite server version 1.9.x. For older versions, please check [previous releases](https://github.com/appwrite/sdk-for-react-native/releases).**

Appwrite is an open-source backend as a service server that abstracts and simplifies complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the React Native SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)

Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@
"build:libs": "rollup -c"
},
"devDependencies": {
"@rollup/plugin-typescript": "8.3.2",
"@rollup/plugin-typescript": "11.1.6",
"@types/json-bigint": "1.0.4",
"playwright": "1.56.1",
"rollup": "2.75.4",
"rollup": "3.29.5",
"serve-handler": "6.1.0",
"tslib": "2.4.0",
"typescript": "^5.3.3"
"tslib": "2.8.1",
"typescript": "5.7.3"
},
"dependencies": {
"expo-file-system": "18.*.*",
Expand Down
3 changes: 2 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pkg from "./package.json";
import { readFileSync } from "fs";
const pkg = JSON.parse(readFileSync("./package.json", "utf8"));
import typescript from "@rollup/plugin-typescript";

const external = Object.keys(pkg.dependencies ?? {});
Expand Down
72 changes: 71 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,17 @@ class Client {
locale: '',
session: '',
devkey: '',
impersonateuserid: '',
impersonateuseremail: '',
impersonateuserphone: '',
platform: '',
};
headers: Headers = {
'x-sdk-name': 'React Native',
'x-sdk-platform': 'client',
'x-sdk-language': 'reactnative',
'x-sdk-version': '0.25.0',
'X-Appwrite-Response-Format': '1.8.0',
'X-Appwrite-Response-Format': '1.9.0',
};

/**
Expand Down Expand Up @@ -293,6 +296,69 @@ class Client {
return this;
}

/**
* Set ImpersonateUserId
*
* Impersonate a user by ID on an already user-authenticated request. Requires
* the current request to be authenticated as a user with impersonator
* capability; X-Appwrite-Key alone is not sufficient. Impersonator users are
* intentionally granted users.read so they can discover a target before
* impersonation begins. Internal audit logs still attribute actions to the
* original impersonator and record the impersonated target only in internal
* audit payload data.
*
* @param value string
*
* @return {this}
*/
setImpersonateUserId(value: string): this {
this.headers['X-Appwrite-Impersonate-User-Id'] = value;
this.config.impersonateuserid = value;
return this;
}

/**
* Set ImpersonateUserEmail
*
* Impersonate a user by email on an already user-authenticated request.
* Requires the current request to be authenticated as a user with
* impersonator capability; X-Appwrite-Key alone is not sufficient.
* Impersonator users are intentionally granted users.read so they can
* discover a target before impersonation begins. Internal audit logs still
* attribute actions to the original impersonator and record the impersonated
* target only in internal audit payload data.
*
* @param value string
*
* @return {this}
*/
setImpersonateUserEmail(value: string): this {
this.headers['X-Appwrite-Impersonate-User-Email'] = value;
this.config.impersonateuseremail = value;
return this;
}

/**
* Set ImpersonateUserPhone
*
* Impersonate a user by phone on an already user-authenticated request.
* Requires the current request to be authenticated as a user with
* impersonator capability; X-Appwrite-Key alone is not sufficient.
* Impersonator users are intentionally granted users.read so they can
* discover a target before impersonation begins. Internal audit logs still
* attribute actions to the original impersonator and record the impersonated
* target only in internal audit payload data.
*
* @param value string
*
* @return {this}
*/
setImpersonateUserPhone(value: string): this {
this.headers['X-Appwrite-Impersonate-User-Phone'] = value;
this.config.impersonateuserphone = value;
return this;
Comment on lines +314 to +359
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep only one impersonation selector active at a time.

Calling these setters sequentially leaves multiple X-Appwrite-Impersonate-* headers and config values live. The next request can carry conflicting selectors instead of the last one the caller chose.

One way to enforce a single active selector
+    private clearImpersonation(): void {
+        delete this.headers['X-Appwrite-Impersonate-User-Id'];
+        delete this.headers['X-Appwrite-Impersonate-User-Email'];
+        delete this.headers['X-Appwrite-Impersonate-User-Phone'];
+        this.config.impersonateuserid = '';
+        this.config.impersonateuseremail = '';
+        this.config.impersonateuserphone = '';
+    }
+
     setImpersonateUserId(value: string): this {
+        this.clearImpersonation();
         this.headers['X-Appwrite-Impersonate-User-Id'] = value;
         this.config.impersonateuserid = value;
         return this;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setImpersonateUserId(value: string): this {
this.headers['X-Appwrite-Impersonate-User-Id'] = value;
this.config.impersonateuserid = value;
return this;
}
/**
* Set ImpersonateUserEmail
*
* Impersonate a user by email on an already user-authenticated request.
* Requires the current request to be authenticated as a user with
* impersonator capability; X-Appwrite-Key alone is not sufficient.
* Impersonator users are intentionally granted users.read so they can
* discover a target before impersonation begins. Internal audit logs still
* attribute actions to the original impersonator and record the impersonated
* target only in internal audit payload data.
*
* @param value string
*
* @return {this}
*/
setImpersonateUserEmail(value: string): this {
this.headers['X-Appwrite-Impersonate-User-Email'] = value;
this.config.impersonateuseremail = value;
return this;
}
/**
* Set ImpersonateUserPhone
*
* Impersonate a user by phone on an already user-authenticated request.
* Requires the current request to be authenticated as a user with
* impersonator capability; X-Appwrite-Key alone is not sufficient.
* Impersonator users are intentionally granted users.read so they can
* discover a target before impersonation begins. Internal audit logs still
* attribute actions to the original impersonator and record the impersonated
* target only in internal audit payload data.
*
* @param value string
*
* @return {this}
*/
setImpersonateUserPhone(value: string): this {
this.headers['X-Appwrite-Impersonate-User-Phone'] = value;
this.config.impersonateuserphone = value;
return this;
private clearImpersonation(): void {
delete this.headers['X-Appwrite-Impersonate-User-Id'];
delete this.headers['X-Appwrite-Impersonate-User-Email'];
delete this.headers['X-Appwrite-Impersonate-User-Phone'];
this.config.impersonateuserid = '';
this.config.impersonateuseremail = '';
this.config.impersonateuserphone = '';
}
setImpersonateUserId(value: string): this {
this.clearImpersonation();
this.headers['X-Appwrite-Impersonate-User-Id'] = value;
this.config.impersonateuserid = value;
return this;
}
/**
* Set ImpersonateUserEmail
*
* Impersonate a user by email on an already user-authenticated request.
* Requires the current request to be authenticated as a user with
* impersonator capability; X-Appwrite-Key alone is not sufficient.
* Impersonator users are intentionally granted users.read so they can
* discover a target before impersonation begins. Internal audit logs still
* attribute actions to the original impersonator and record the impersonated
* target only in internal audit payload data.
*
* `@param` value string
*
* `@return` {this}
*/
setImpersonateUserEmail(value: string): this {
this.headers['X-Appwrite-Impersonate-User-Email'] = value;
this.config.impersonateuseremail = value;
return this;
}
/**
* Set ImpersonateUserPhone
*
* Impersonate a user by phone on an already user-authenticated request.
* Requires the current request to be authenticated as a user with
* impersonator capability; X-Appwrite-Key alone is not sufficient.
* Impersonator users are intentionally granted users.read so they can
* discover a target before impersonation begins. Internal audit logs still
* attribute actions to the original impersonator and record the impersonated
* target only in internal audit payload data.
*
* `@param` value string
*
* `@return` {this}
*/
setImpersonateUserPhone(value: string): this {
this.headers['X-Appwrite-Impersonate-User-Phone'] = value;
this.config.impersonateuserphone = value;
return this;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client.ts` around lines 314 - 359, When setting an impersonation selector
in setImpersonateUserId, setImpersonateUserEmail, or setImpersonateUserPhone,
clear the other impersonation headers and config fields so only the newly-set
selector remains active: remove the other
'X-Appwrite-Impersonate-User-Id|Email|Phone' keys from this.headers and unset
the corresponding
this.config.impersonateuserid|impersonateuseremail|impersonateuserphone fields
before assigning the new value in the respective method (refer to the three
setter methods by name to locate the changes).

}


private realtime: Realtime = {
socket: undefined,
Expand Down Expand Up @@ -588,6 +654,10 @@ class Client {
window.localStorage.setItem('cookieFallback', cookieFallback);
}

if (data && typeof data === 'object') {
data.toString = () => JSONbig.stringify(data);
}
Comment on lines +657 to +659
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/client.ts | sed -n '650,670p'

Repository: appwrite/sdk-for-react-native

Length of output: 995


🏁 Script executed:

cat -n src/client.ts | sed -n '630,670p'

Repository: appwrite/sdk-for-react-native

Length of output: 1965


🏁 Script executed:

python3 << 'EOF'
# Verify enumerable property behavior
code = """
// Direct property assignment (current approach)
const obj1 = { name: 'test' };
obj1.toString = () => 'custom';

console.log('Direct assignment:');
console.log('Object.keys includes toString:', Object.keys(obj1).includes('toString'));
console.log('for...in includes toString:');
for (let key in obj1) {
  console.log('  -', key);
}

// Using Object.defineProperty with enumerable: false (proposed approach)
const obj2 = { name: 'test' };
Object.defineProperty(obj2, 'toString', {
  value: () => 'custom',
  enumerable: false,
  configurable: true
});

console.log('\\nObject.defineProperty with enumerable: false:');
console.log('Object.keys includes toString:', Object.keys(obj2).includes('toString'));
console.log('for...in includes toString:');
for (let key in obj2) {
  console.log('  -', key);
}

// Check if existing toString is overwritten
const obj3 = { toString: 'original' };
obj3.toString = () => 'overwritten';
console.log('\\nOverwriting existing toString:', obj3.toString);
"""

print("JavaScript enumerable property behavior:")
print(code)
print("\nThis demonstrates that:")
print("1. Direct assignment creates enumerable properties (included in Object.keys/for...in)")
print("2. Object.defineProperty with enumerable:false excludes from enumeration")
print("3. Direct assignment overwrites existing properties")
EOF

Repository: appwrite/sdk-for-react-native

Length of output: 1337


🏁 Script executed:

cat -n src/client.ts | sed -n '620,665p'

Repository: appwrite/sdk-for-react-native

Length of output: 2251


Remove the enumerable toString property assignment from response objects.

The current code mutates every response object by adding an enumerable toString property, which causes it to appear in Object.keys() and for...in loops. More critically, it overwrites any legitimate toString field that may exist in the server response data (since this parses JSON directly from the API response).

Use Object.defineProperty() with enumerable: false to add the custom method without polluting enumeration, and check for existing toString fields first:

Suggested fix
-            if (data && typeof data === 'object') {
-                data.toString = () => JSONbig.stringify(data);
-            }
+            if (
+                data &&
+                typeof data === 'object' &&
+                !Object.prototype.hasOwnProperty.call(data, 'toString')
+            ) {
+                Object.defineProperty(data, 'toString', {
+                    value: () => JSONbig.stringify(data),
+                    enumerable: false,
+                    configurable: true,
+                });
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (data && typeof data === 'object') {
data.toString = () => JSONbig.stringify(data);
}
if (
data &&
typeof data === 'object' &&
!Object.prototype.hasOwnProperty.call(data, 'toString')
) {
Object.defineProperty(data, 'toString', {
value: () => JSONbig.stringify(data),
enumerable: false,
configurable: true,
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client.ts` around lines 657 - 659, The code currently mutates response
objects by assigning an enumerable toString property via "data.toString = () =>
JSONbig.stringify(data)" which pollutes enumeration and can overwrite an
existing toString; instead, before adding a method, check if
Object.prototype.hasOwnProperty.call(data, 'toString') is false (or typeof
data.toString !== 'function'), and then add the method with
Object.defineProperty(data, 'toString', { value: () => JSONbig.stringify(data),
writable: true, configurable: true, enumerable: false }); this keeps the custom
toString non-enumerable and preserves any existing toString from the server.


return data;
} catch (e) {
if (e instanceof AppwriteException) {
Expand Down
18 changes: 13 additions & 5 deletions src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export namespace Models {
/**
* Row sequence ID.
*/
$sequence: number;
$sequence: string;
/**
* Table ID.
*/
Expand Down Expand Up @@ -279,7 +279,7 @@ export namespace Models {
/**
* Document sequence ID.
*/
$sequence: number;
$sequence: string;
/**
* Collection ID.
*/
Expand Down Expand Up @@ -316,15 +316,15 @@ export namespace Models {
*/
event: string;
/**
* User ID.
* User ID of the actor recorded for this log. During impersonation, this is the original impersonator, not the impersonated target user.
*/
userId: string;
/**
* User Email.
* User email of the actor recorded for this log. During impersonation, this is the original impersonator.
*/
userEmail: string;
/**
* User Name.
* User name of the actor recorded for this log. During impersonation, this is the original impersonator.
*/
userName: string;
/**
Expand Down Expand Up @@ -477,6 +477,14 @@ export namespace Models {
* Most recent access date in ISO 8601 format. This attribute is only updated again after 24 hours.
*/
accessedAt: string;
/**
* Whether the user can impersonate other users.
*/
impersonator?: boolean;
/**
* ID of the original actor performing the impersonation. Present only when the current request is impersonating another user. Internal audit logs attribute the action to this user, while the impersonated target is recorded only in internal audit payload data.
*/
impersonatorUserId?: string;
}

/**
Expand Down