mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-20 20:26:55 -05:00
fix(expo): chunk storage values by byte size
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@better-auth/expo": patch
|
||||
---
|
||||
|
||||
Store multibyte session data safely in Expo secure storage.
|
||||
@@ -25,14 +25,41 @@ export function normalizeCookieName(name: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Character budget per stored chunk. Some native stores reject large values,
|
||||
* so larger strings are split across keys. This is not a byte-size guarantee.
|
||||
* UTF-8 byte budget per stored chunk. Some native stores reject large values,
|
||||
* so larger strings are split across keys.
|
||||
*
|
||||
* @see https://github.com/better-auth/better-auth/issues/9151
|
||||
*/
|
||||
const STORAGE_VALUE_LIMIT = 1800;
|
||||
const MAX_STORAGE_CHUNKS = 100;
|
||||
|
||||
function getUtf8ByteLength(character: string) {
|
||||
const codePoint = character.codePointAt(0) ?? 0;
|
||||
if (codePoint <= 0x7f) return 1;
|
||||
if (codePoint <= 0x7ff) return 2;
|
||||
if (codePoint <= 0xffff) return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
function splitStorageValue(value: string) {
|
||||
const chunks: string[] = [];
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
let bytes = 0;
|
||||
for (const character of value) {
|
||||
const characterBytes = getUtf8ByteLength(character);
|
||||
if (bytes + characterBytes > STORAGE_VALUE_LIMIT) {
|
||||
chunks.push(value.slice(start, end));
|
||||
start = end;
|
||||
bytes = 0;
|
||||
}
|
||||
bytes += characterBytes;
|
||||
end += character.length;
|
||||
}
|
||||
chunks.push(value.slice(start));
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a base key whose value is split across multiple storage keys. Legacy
|
||||
* markers contain only the chunk count. Current markers also identify the
|
||||
@@ -202,14 +229,15 @@ function getStorageWritePlan(
|
||||
const currentMarker = currentBaseValue?.startsWith(CHUNK_MARKER)
|
||||
? parseChunkMarker(currentBaseValue)
|
||||
: null;
|
||||
if (value.length <= STORAGE_VALUE_LIMIT) {
|
||||
const chunks = splitStorageValue(value);
|
||||
if (chunks.length === 1) {
|
||||
return {
|
||||
writes: [[key, value]],
|
||||
cleanup: getUnusedChunkRanges(key, currentMarker, null),
|
||||
};
|
||||
}
|
||||
|
||||
const count = Math.ceil(value.length / STORAGE_VALUE_LIMIT);
|
||||
const count = chunks.length;
|
||||
if (count > MAX_STORAGE_CHUNKS) {
|
||||
throw new Error(
|
||||
`Storage value requires ${count} chunks, exceeding the limit of ${MAX_STORAGE_CHUNKS}`,
|
||||
@@ -230,12 +258,8 @@ function getStorageWritePlan(
|
||||
serializeChunkMarker({ ...currentMarker, fallbackCount: null }),
|
||||
]);
|
||||
}
|
||||
for (let i = 0; i < count; i++) {
|
||||
const start = i * STORAGE_VALUE_LIMIT;
|
||||
writes.push([
|
||||
getChunkKey(key, marker, i),
|
||||
value.slice(start, start + STORAGE_VALUE_LIMIT),
|
||||
]);
|
||||
for (const [index, chunk] of chunks.entries()) {
|
||||
writes.push([getChunkKey(key, marker, index), chunk]);
|
||||
}
|
||||
writes.push([key, serializeChunkMarker(marker)]);
|
||||
return { writes, cleanup: getUnusedChunkRanges(key, currentMarker, marker) };
|
||||
|
||||
@@ -90,6 +90,29 @@ it("should round-trip a value larger than the per-write storage limit", async ()
|
||||
expect(storedValue).toBe(large);
|
||||
});
|
||||
|
||||
/**
|
||||
* @see https://docs.expo.dev/versions/latest/sdk/securestore/#securestoresetitemasynckey-value-options
|
||||
*/
|
||||
it("should keep Unicode chunks within the storage byte limit", () => {
|
||||
const map = new Map<string, string>();
|
||||
const storage = storageAdapter({
|
||||
getItem: (name) => map.get(name) ?? null,
|
||||
setItem: (name, value) => map.set(name, value),
|
||||
getItemAsync: async (name) => map.get(name) ?? null,
|
||||
setItemAsync: async (name, value) => {
|
||||
map.set(name, value);
|
||||
},
|
||||
});
|
||||
const value = `${"x".repeat(1_799)}🔐${"🙂".repeat(1_000)}`;
|
||||
|
||||
storage.setItem("better-auth_cookie", value);
|
||||
|
||||
for (const chunk of map.values()) {
|
||||
expect(new TextEncoder().encode(chunk).length).toBeLessThanOrEqual(1_800);
|
||||
}
|
||||
expect(storage.getItem("better-auth_cookie")).toBe(value);
|
||||
});
|
||||
|
||||
it("should store a value within the limit under the base key unchanged", () => {
|
||||
const map = new Map<string, string>();
|
||||
const storage = storageAdapter({
|
||||
|
||||
Reference in New Issue
Block a user