From 88bd390e1399f38dcb14e13142271f0c3d52ace1 Mon Sep 17 00:00:00 2001 From: Rajesh Gollapudi Date: Tue, 10 Dec 2024 13:33:24 -0500 Subject: [PATCH 1/6] feature: add sqlite storage --- .gitignore | 2 +- bun.lockb | Bin 265960 -> 269776 bytes examples/authorizer/sqlite/.gitignore | 1 + examples/authorizer/sqlite/authorizer.ts | 29 ++++++++++++++ packages/openauth/package.json | 3 +- packages/openauth/src/authorizer.ts | 2 + packages/openauth/src/storage/sqlite.ts | 48 +++++++++++++++++++++++ 7 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 examples/authorizer/sqlite/.gitignore create mode 100644 examples/authorizer/sqlite/authorizer.ts create mode 100644 packages/openauth/src/storage/sqlite.ts diff --git a/.gitignore b/.gitignore index f09c7c2..d889474 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/node_modules +node_modules .sst .env dist diff --git a/bun.lockb b/bun.lockb index 10e7d3d50f091882269f2c611d439d90e2b0ce5d..2669d2e429d3f37d84c11727cad23fd9a5600a43 100755 GIT binary patch delta 45523 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 { + console.log(email, code); + }, + }) + ), + }, + success: async (ctx, value) => { + if (value.provider === "password") { + return ctx.subject("user", { + email: value.email, + }); + } + throw new Error("Invalid provider"); + }, +}); diff --git a/packages/openauth/package.json b/packages/openauth/package.json index 473a8c4..7530f37 100644 --- a/packages/openauth/package.json +++ b/packages/openauth/package.json @@ -36,7 +36,8 @@ "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", - "jose": "5.9.6" + "jose": "5.9.6", + "libsql": "^0.5.0-pre.3" }, "files": [ "src", diff --git a/packages/openauth/src/authorizer.ts b/packages/openauth/src/authorizer.ts index e935c1b..1427f75 100644 --- a/packages/openauth/src/authorizer.ts +++ b/packages/openauth/src/authorizer.ts @@ -45,6 +45,7 @@ import { getTheme, setTheme, Theme } from "./ui/theme.js"; import { isDomainMatch } from "./util.js"; import { DynamoStorage } from "./storage/dynamo.js"; import { MemoryStorage } from "./storage/memory.js"; +import { SQLiteStorage } from "./storage/sqlite.js"; export const aws = awsHandle; @@ -125,6 +126,7 @@ export function authorizer< const parsed = JSON.parse(process.env.OPENAUTH_STORAGE); if (parsed.type === "dynamo") storage = DynamoStorage(parsed.options); if (parsed.type === "memory") storage = MemoryStorage(); + if (parsed.type === "sqlite") storage = SQLiteStorage(); if (parsed.type === "cloudflare") throw new Error( "Cloudflare storage cannot be configured through env because it requires bindings." diff --git a/packages/openauth/src/storage/sqlite.ts b/packages/openauth/src/storage/sqlite.ts new file mode 100644 index 0000000..99db235 --- /dev/null +++ b/packages/openauth/src/storage/sqlite.ts @@ -0,0 +1,48 @@ +import { + joinKey, + splitKey, + type StorageAdapter, +} from "@openauthjs/openauth/storage/storage"; + +import Database from "libsql"; + +export interface SqliteStorageOptions { + persist?: string; +} +export function SQLiteStorage(input?: SqliteStorageOptions): StorageAdapter { + // initialize sqlite database and create the necessary table structure + const db = new Database(input?.persist || ":memory:"); + db.exec( + "CREATE TABLE IF NOT EXISTS storage (key TEXT PRIMARY KEY, value TEXT, ttl INTEGER)" + ); + + return { + async get(key: string[]) { + const joined = joinKey(key); + const row = db + .prepare("SELECT value FROM storage WHERE key = ?") + .get(joined) as { value: string } | undefined; + return row ? JSON.parse(row.value) : undefined; + }, + async set(key: string[], value: any, ttl?: number) { + const joined = joinKey(key); + db.prepare( + "INSERT OR REPLACE INTO storage (key, value, ttl) VALUES (?, ?, ?)" + ).run(joined, JSON.stringify(value), ttl); + }, + async remove(key: string[]) { + const joined = joinKey(key); + db.prepare("DELETE FROM storage WHERE key = ?").run(joined); + }, + async *scan(prefix: string[]) { + const joined = joinKey(prefix); + const rows = db + .prepare("SELECT key, value FROM storage WHERE key LIKE ?") + .all(joined + "%") as { key: string; value: string }[]; + + for (const row of rows) { + yield [splitKey(row.key), JSON.parse(row.value)]; + } + }, + }; +} From 103aafaaf9e1bce08b3b1f3c93cd892bafe52c67 Mon Sep 17 00:00:00 2001 From: Rajesh Gollapudi Date: Tue, 10 Dec 2024 13:45:08 -0500 Subject: [PATCH 2/6] Return ttl as part of scan --- packages/openauth/src/storage/sqlite.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/openauth/src/storage/sqlite.ts b/packages/openauth/src/storage/sqlite.ts index 99db235..c4149c3 100644 --- a/packages/openauth/src/storage/sqlite.ts +++ b/packages/openauth/src/storage/sqlite.ts @@ -37,8 +37,8 @@ export function SQLiteStorage(input?: SqliteStorageOptions): StorageAdapter { async *scan(prefix: string[]) { const joined = joinKey(prefix); const rows = db - .prepare("SELECT key, value FROM storage WHERE key LIKE ?") - .all(joined + "%") as { key: string; value: string }[]; + .prepare("SELECT key, value, ttl FROM storage WHERE key LIKE ?") + .all(joined + "%") as { key: string; value: string; ttl: number }[]; for (const row of rows) { yield [splitKey(row.key), JSON.parse(row.value)]; From ceeb91ae097ffc52ec2e83d1751e8a2ea6770855 Mon Sep 17 00:00:00 2001 From: Rajesh Gollapudi Date: Tue, 10 Dec 2024 17:17:52 -0500 Subject: [PATCH 3/6] Make tableName configurable --- packages/openauth/src/storage/sqlite.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/openauth/src/storage/sqlite.ts b/packages/openauth/src/storage/sqlite.ts index c4149c3..b341a2d 100644 --- a/packages/openauth/src/storage/sqlite.ts +++ b/packages/openauth/src/storage/sqlite.ts @@ -8,36 +8,40 @@ import Database from "libsql"; export interface SqliteStorageOptions { persist?: string; + tableName?: string; } + export function SQLiteStorage(input?: SqliteStorageOptions): StorageAdapter { // initialize sqlite database and create the necessary table structure - const db = new Database(input?.persist || ":memory:"); + const db = new Database(input?.persist ?? ":memory:"); + const TABLE_NAME = input?.tableName ?? "__openauth__kv_storage"; + db.exec( - "CREATE TABLE IF NOT EXISTS storage (key TEXT PRIMARY KEY, value TEXT, ttl INTEGER)" + `CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (key TEXT PRIMARY KEY, value TEXT, ttl INTEGER)` ); return { async get(key: string[]) { const joined = joinKey(key); const row = db - .prepare("SELECT value FROM storage WHERE key = ?") + .prepare(`SELECT value FROM ${TABLE_NAME} WHERE key = ?`) .get(joined) as { value: string } | undefined; return row ? JSON.parse(row.value) : undefined; }, async set(key: string[], value: any, ttl?: number) { const joined = joinKey(key); db.prepare( - "INSERT OR REPLACE INTO storage (key, value, ttl) VALUES (?, ?, ?)" + `INSERT OR REPLACE INTO ${TABLE_NAME} (key, value, ttl) VALUES (?, ?, ?)` ).run(joined, JSON.stringify(value), ttl); }, async remove(key: string[]) { const joined = joinKey(key); - db.prepare("DELETE FROM storage WHERE key = ?").run(joined); + db.prepare(`DELETE FROM ${TABLE_NAME} WHERE key = ?`).run(joined); }, async *scan(prefix: string[]) { const joined = joinKey(prefix); const rows = db - .prepare("SELECT key, value, ttl FROM storage WHERE key LIKE ?") + .prepare(`SELECT key, value, ttl FROM ${TABLE_NAME} WHERE key LIKE ?`) .all(joined + "%") as { key: string; value: string; ttl: number }[]; for (const row of rows) { From 47fe978eb9d4da6f42dd298cc650f81946bdef9b Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 10 Dec 2024 18:31:03 -0500 Subject: [PATCH 4/6] add sqlite storage adapter --- .changeset/orange-lemons-sip.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/orange-lemons-sip.md diff --git a/.changeset/orange-lemons-sip.md b/.changeset/orange-lemons-sip.md new file mode 100644 index 0000000..2269b02 --- /dev/null +++ b/.changeset/orange-lemons-sip.md @@ -0,0 +1,5 @@ +--- +"@openauthjs/openauth": patch +--- + +add sqlite storage adapter From 00893b096188efc78a07319b0ef1420191dd8f6a Mon Sep 17 00:00:00 2001 From: Rajesh Gollapudi Date: Tue, 10 Dec 2024 19:47:24 -0500 Subject: [PATCH 5/6] cleanup expired values --- packages/openauth/src/storage/sqlite.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/openauth/src/storage/sqlite.ts b/packages/openauth/src/storage/sqlite.ts index b341a2d..bcf4f12 100644 --- a/packages/openauth/src/storage/sqlite.ts +++ b/packages/openauth/src/storage/sqlite.ts @@ -3,7 +3,6 @@ import { splitKey, type StorageAdapter, } from "@openauthjs/openauth/storage/storage"; - import Database from "libsql"; export interface SqliteStorageOptions { @@ -20,8 +19,13 @@ export function SQLiteStorage(input?: SqliteStorageOptions): StorageAdapter { `CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (key TEXT PRIMARY KEY, value TEXT, ttl INTEGER)` ); + const cleanExpired = () => { + db.prepare(`DELETE FROM ${TABLE_NAME} WHERE ttl < ?`).run(Date.now()); + }; + return { async get(key: string[]) { + cleanExpired(); const joined = joinKey(key); const row = db .prepare(`SELECT value FROM ${TABLE_NAME} WHERE key = ?`) @@ -39,6 +43,7 @@ export function SQLiteStorage(input?: SqliteStorageOptions): StorageAdapter { db.prepare(`DELETE FROM ${TABLE_NAME} WHERE key = ?`).run(joined); }, async *scan(prefix: string[]) { + cleanExpired(); const joined = joinKey(prefix); const rows = db .prepare(`SELECT key, value, ttl FROM ${TABLE_NAME} WHERE key LIKE ?`) From bd9ce7b563ca640c0c0909fcfede3a0d564a06b5 Mon Sep 17 00:00:00 2001 From: Rajesh Gollapudi Date: Wed, 11 Dec 2024 10:26:57 -0500 Subject: [PATCH 6/6] Add tests for sqlite storage --- packages/openauth/src/storage/sqlite.ts | 38 +++++---- packages/openauth/test/sqlite-storage.test.ts | 84 +++++++++++++++++++ 2 files changed, 104 insertions(+), 18 deletions(-) create mode 100644 packages/openauth/test/sqlite-storage.test.ts diff --git a/packages/openauth/src/storage/sqlite.ts b/packages/openauth/src/storage/sqlite.ts index bcf4f12..0e97d74 100644 --- a/packages/openauth/src/storage/sqlite.ts +++ b/packages/openauth/src/storage/sqlite.ts @@ -1,8 +1,4 @@ -import { - joinKey, - splitKey, - type StorageAdapter, -} from "@openauthjs/openauth/storage/storage"; +import { joinKey, splitKey, type StorageAdapter } from "./storage.js"; import Database from "libsql"; export interface SqliteStorageOptions { @@ -16,40 +12,46 @@ export function SQLiteStorage(input?: SqliteStorageOptions): StorageAdapter { const TABLE_NAME = input?.tableName ?? "__openauth__kv_storage"; db.exec( - `CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (key TEXT PRIMARY KEY, value TEXT, ttl INTEGER)` + `CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (key TEXT PRIMARY KEY, value TEXT, expiry INTEGER)` ); - const cleanExpired = () => { - db.prepare(`DELETE FROM ${TABLE_NAME} WHERE ttl < ?`).run(Date.now()); - }; - return { async get(key: string[]) { - cleanExpired(); const joined = joinKey(key); + const row = db - .prepare(`SELECT value FROM ${TABLE_NAME} WHERE key = ?`) - .get(joined) as { value: string } | undefined; + .prepare(`SELECT value, expiry FROM ${TABLE_NAME} WHERE key = ?`) + .get(joined) as { value: string; expiry: number } | undefined; + + if (row && row.expiry && row.expiry < Date.now()) { + db.prepare(`DELETE FROM ${TABLE_NAME} WHERE key = ?`).run(joined); + return undefined; + } return row ? JSON.parse(row.value) : undefined; }, async set(key: string[], value: any, ttl?: number) { + const expiry = ttl ? Date.now() + ttl * 1000 : undefined; const joined = joinKey(key); db.prepare( - `INSERT OR REPLACE INTO ${TABLE_NAME} (key, value, ttl) VALUES (?, ?, ?)` - ).run(joined, JSON.stringify(value), ttl); + `INSERT OR REPLACE INTO ${TABLE_NAME} (key, value, expiry) VALUES (?, ?, ?)` + ).run(joined, JSON.stringify(value), expiry); }, async remove(key: string[]) { const joined = joinKey(key); db.prepare(`DELETE FROM ${TABLE_NAME} WHERE key = ?`).run(joined); }, async *scan(prefix: string[]) { - cleanExpired(); const joined = joinKey(prefix); const rows = db - .prepare(`SELECT key, value, ttl FROM ${TABLE_NAME} WHERE key LIKE ?`) - .all(joined + "%") as { key: string; value: string; ttl: number }[]; + .prepare( + `SELECT key, value, expiry FROM ${TABLE_NAME} WHERE key LIKE ?` + ) + .all(joined + "%") as { key: string; value: string; expiry: number }[]; for (const row of rows) { + if (row.expiry && row.expiry < Date.now()) { + continue; + } yield [splitKey(row.key), JSON.parse(row.value)]; } }, diff --git a/packages/openauth/test/sqlite-storage.test.ts b/packages/openauth/test/sqlite-storage.test.ts new file mode 100644 index 0000000..94a5a58 --- /dev/null +++ b/packages/openauth/test/sqlite-storage.test.ts @@ -0,0 +1,84 @@ +import { beforeEach, describe, expect, test } from "bun:test"; +import { SQLiteStorage } from "../src/storage/sqlite"; + +let storage = SQLiteStorage(); + +beforeEach(async () => { + storage = SQLiteStorage(); +}); + +describe("set", () => { + test("basic", async () => { + await storage.set(["users", "123"], { name: "Test User" }); + const result = await storage.get(["users", "123"]); + expect(result).toEqual({ name: "Test User" }); + }); + + test("ttl", async () => { + await storage.set(["temp", "key"], { value: "value" }, 0.1); // 100ms TTL + let result = await storage.get(["temp", "key"]); + expect(result?.value).toBe("value"); + + await new Promise((resolve) => setTimeout(resolve, 150)); + result = await storage.get(["temp", "key"]); + expect(result).toBeUndefined(); + }); + + test("nested", async () => { + const complexObj = { + id: 1, + nested: { a: 1, b: { c: 2 } }, + array: [1, 2, 3], + }; + await storage.set(["complex"], complexObj); + const result = await storage.get(["complex"]); + expect(result).toEqual(complexObj); + }); +}); + +describe("get", () => { + test("missing", async () => { + const result = await storage.get(["nonexistent"]); + expect(result).toBeUndefined(); + }); + + test("key", async () => { + await storage.set(["a", "b", "c"], { value: "nested" }); + const result = await storage.get(["a", "b", "c"]); + expect(result?.value).toBe("nested"); + }); +}); + +describe("remove", () => { + test("existing", async () => { + await storage.set(["test"], "value"); + await storage.remove(["test"]); + const result = await storage.get(["test"]); + expect(result).toBeUndefined(); + }); + + test("missing", async () => { + expect(storage.remove(["nonexistent"])).resolves.toBeUndefined(); + }); +}); + +describe("scan", () => { + test("all", async () => { + await storage.set(["users", "1"], { id: 1 }); + await storage.set(["users", "2"], { id: 2 }); + await storage.set(["other"], { id: 3 }); + const results = await Array.fromAsync(storage.scan(["users"])); + expect(results).toHaveLength(2); + expect(results).toContainEqual([["users", "1"], { id: 1 }]); + expect(results).toContainEqual([["users", "2"], { id: 2 }]); + }); + + test("ttl", async () => { + await storage.set(["temp", "1"], "a", 0.1); + await storage.set(["temp", "2"], "b", 0.1); + await storage.set(["temp", "3"], "c"); + expect(await Array.fromAsync(storage.scan(["temp"]))).toHaveLength(3); + await new Promise((resolve) => setTimeout(resolve, 150)); + expect(await Array.fromAsync(storage.scan(["temp"]))).toHaveLength(1); + }); +});