+4
-1
packages/oauth/deno.json
+4
-1
packages/oauth/deno.json
···
1
1
{
2
2
"name": "@slices/oauth",
3
-
"version": "0.6.0",
3
+
"version": "0.7.0-alpha.4",
4
4
"exports": {
5
5
".": "./mod.ts"
6
+
},
7
+
"imports": {
8
+
"@libsql/client": "npm:@libsql/client@0.6.0"
6
9
},
7
10
"compilerOptions": {
8
11
"lib": ["deno.ns", "deno.unstable", "dom", "dom.iterable", "esnext"]
+1
packages/oauth/mod.ts
+1
packages/oauth/mod.ts
···
3
3
export { DeviceFlowClient } from "./src/device.ts";
4
4
export { DenoKVOAuthStorage } from "./src/storage/deno-kv.ts";
5
5
export { SQLiteOAuthStorage } from "./src/storage/sqlite.ts";
6
+
export { ValTownSQLiteOAuthStorage } from "./src/storage/valtown-sqlite.ts";
6
7
export type {
7
8
OAuthConfig,
8
9
OAuthTokens,
+149
packages/oauth/src/storage/valtown-sqlite.ts
+149
packages/oauth/src/storage/valtown-sqlite.ts
···
1
+
import type { OAuthStorage, OAuthTokens } from "../types.ts";
2
+
import type { InStatement, TransactionMode } from "@libsql/client";
3
+
4
+
// Val Town's SQLite ResultSet (doesn't have toJSON method)
5
+
interface ValTownResultSet {
6
+
columns: string[];
7
+
columnTypes: string[];
8
+
rows: unknown[][];
9
+
rowsAffected: number;
10
+
lastInsertRowid?: bigint;
11
+
}
12
+
13
+
interface SQLiteInstance {
14
+
execute(statement: InStatement): Promise<ValTownResultSet>;
15
+
batch(
16
+
statements: InStatement[],
17
+
mode?: TransactionMode,
18
+
): Promise<ValTownResultSet[]>;
19
+
}
20
+
21
+
export class ValTownSQLiteOAuthStorage implements OAuthStorage {
22
+
private sqlite: SQLiteInstance;
23
+
24
+
constructor(sqlite: SQLiteInstance) {
25
+
this.sqlite = sqlite;
26
+
this.initTables();
27
+
}
28
+
29
+
private async initTables(): Promise<void> {
30
+
// Create tokens table
31
+
await this.sqlite.execute(`
32
+
CREATE TABLE IF NOT EXISTS oauth_tokens (
33
+
id INTEGER PRIMARY KEY,
34
+
session_id TEXT,
35
+
access_token TEXT NOT NULL,
36
+
token_type TEXT NOT NULL,
37
+
expires_at INTEGER,
38
+
refresh_token TEXT,
39
+
scope TEXT,
40
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
41
+
UNIQUE(session_id)
42
+
)
43
+
`);
44
+
45
+
// Create states table with automatic cleanup
46
+
await this.sqlite.execute(`
47
+
CREATE TABLE IF NOT EXISTS oauth_states (
48
+
state TEXT PRIMARY KEY,
49
+
code_verifier TEXT NOT NULL,
50
+
timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
51
+
)
52
+
`);
53
+
54
+
// Create index for cleanup efficiency
55
+
await this.sqlite.execute(`
56
+
CREATE INDEX IF NOT EXISTS idx_oauth_states_timestamp ON oauth_states(timestamp)
57
+
`);
58
+
}
59
+
60
+
async getTokens(sessionId: string): Promise<OAuthTokens | null> {
61
+
const result = await this.sqlite.execute({
62
+
sql: `SELECT access_token, token_type, expires_at, refresh_token, scope
63
+
FROM oauth_tokens
64
+
WHERE session_id = ?
65
+
LIMIT 1`,
66
+
args: [sessionId],
67
+
});
68
+
69
+
if (result.rows.length === 0) return null;
70
+
71
+
const row = result.rows[0];
72
+
73
+
return {
74
+
accessToken: row[0] as string,
75
+
tokenType: row[1] as string,
76
+
expiresAt: (row[2] as number | null) ?? undefined,
77
+
refreshToken: (row[3] as string | null) ?? undefined,
78
+
scope: (row[4] as string | null) ?? undefined,
79
+
};
80
+
}
81
+
82
+
async setTokens(tokens: OAuthTokens, sessionId: string): Promise<void> {
83
+
await this.clearTokens(sessionId);
84
+
85
+
await this.sqlite.execute({
86
+
sql:
87
+
`INSERT INTO oauth_tokens (session_id, access_token, token_type, expires_at, refresh_token, scope)
88
+
VALUES (?, ?, ?, ?, ?, ?)`,
89
+
args: [
90
+
sessionId,
91
+
tokens.accessToken,
92
+
tokens.tokenType,
93
+
tokens.expiresAt ?? null,
94
+
tokens.refreshToken ?? null,
95
+
tokens.scope ?? null,
96
+
],
97
+
});
98
+
}
99
+
100
+
async clearTokens(sessionId: string): Promise<void> {
101
+
await this.sqlite.execute({
102
+
sql: "DELETE FROM oauth_tokens WHERE session_id = ?",
103
+
args: [sessionId],
104
+
});
105
+
}
106
+
107
+
async getState(state: string): Promise<string | null> {
108
+
const result = await this.sqlite.execute({
109
+
sql: "SELECT code_verifier FROM oauth_states WHERE state = ?",
110
+
args: [state],
111
+
});
112
+
113
+
if (result.rows.length === 0) return null;
114
+
115
+
const codeVerifier = result.rows[0][0] as string;
116
+
117
+
// Delete after use (one-time use)
118
+
await this.clearState(state);
119
+
120
+
return codeVerifier;
121
+
}
122
+
123
+
async setState(state: string, codeVerifier: string): Promise<void> {
124
+
await this.sqlite.execute({
125
+
sql:
126
+
`INSERT OR REPLACE INTO oauth_states (state, code_verifier, timestamp)
127
+
VALUES (?, ?, ?)`,
128
+
args: [state, codeVerifier, Date.now()],
129
+
});
130
+
131
+
// Auto-cleanup expired states
132
+
await this.cleanup();
133
+
}
134
+
135
+
async clearState(state: string): Promise<void> {
136
+
await this.sqlite.execute({
137
+
sql: "DELETE FROM oauth_states WHERE state = ?",
138
+
args: [state],
139
+
});
140
+
}
141
+
142
+
private async cleanup(): Promise<void> {
143
+
const cutoff = Date.now() - (10 * 60 * 1000); // 10 minutes ago
144
+
await this.sqlite.execute({
145
+
sql: "DELETE FROM oauth_states WHERE timestamp < ?",
146
+
args: [cutoff],
147
+
});
148
+
}
149
+
}
+3
-2
packages/session/deno.json
+3
-2
packages/session/deno.json
···
1
1
{
2
2
"name": "@slices/session",
3
-
"version": "0.3.0",
3
+
"version": "0.4.0-alpha.4",
4
4
"exports": "./mod.ts",
5
5
"compilerOptions": {
6
6
"lib": ["deno.ns", "deno.unstable", "esnext", "dom"]
···
10
10
"check": "deno check mod.ts"
11
11
},
12
12
"imports": {
13
-
"pg": "npm:pg@^8.16.3"
13
+
"pg": "npm:pg@^8.16.3",
14
+
"@libsql/client": "npm:@libsql/client@0.6.0"
14
15
}
15
16
}
+1
packages/session/mod.ts
+1
packages/session/mod.ts
···
1
1
export { SessionStore } from "./src/store.ts";
2
2
export { MemoryAdapter } from "./src/adapters/memory.ts";
3
3
export { SQLiteAdapter } from "./src/adapters/sqlite.ts";
4
+
export { ValTownSQLiteAdapter } from "./src/adapters/valtown-sqlite.ts";
4
5
export { PostgresAdapter } from "./src/adapters/postgres.ts";
5
6
export { DenoKVAdapter } from "./src/adapters/deno-kv.ts";
6
7
export { withOAuthSession } from "./src/oauth-integration.ts";
+180
packages/session/src/adapters/valtown-sqlite.ts
+180
packages/session/src/adapters/valtown-sqlite.ts
···
1
+
import type { SessionAdapter, SessionData } from "../types.ts";
2
+
import type { InStatement, InValue, TransactionMode } from "@libsql/client";
3
+
4
+
// Val Town's SQLite ResultSet (doesn't have toJSON method)
5
+
interface ValTownResultSet {
6
+
columns: string[];
7
+
columnTypes: string[];
8
+
rows: unknown[][];
9
+
rowsAffected: number;
10
+
lastInsertRowid?: bigint;
11
+
}
12
+
13
+
interface SQLiteInstance {
14
+
execute(statement: InStatement): Promise<ValTownResultSet>;
15
+
batch(statements: InStatement[], mode?: TransactionMode): Promise<ValTownResultSet[]>;
16
+
}
17
+
18
+
export class ValTownSQLiteAdapter implements SessionAdapter {
19
+
private sqlite: SQLiteInstance;
20
+
21
+
constructor(sqlite: SQLiteInstance) {
22
+
this.sqlite = sqlite;
23
+
this.initializeDatabase();
24
+
}
25
+
26
+
private async initializeDatabase(): Promise<void> {
27
+
await this.sqlite.execute(`
28
+
CREATE TABLE IF NOT EXISTS sessions (
29
+
session_id TEXT PRIMARY KEY,
30
+
user_id TEXT NOT NULL,
31
+
handle TEXT,
32
+
is_authenticated INTEGER NOT NULL DEFAULT 1,
33
+
data TEXT, -- JSON string
34
+
created_at INTEGER NOT NULL,
35
+
expires_at INTEGER NOT NULL,
36
+
last_accessed_at INTEGER NOT NULL
37
+
)
38
+
`);
39
+
40
+
// Index for cleanup operations
41
+
await this.sqlite.execute(`
42
+
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at
43
+
ON sessions(expires_at)
44
+
`);
45
+
46
+
// Index for user lookups
47
+
await this.sqlite.execute(`
48
+
CREATE INDEX IF NOT EXISTS idx_sessions_user_id
49
+
ON sessions(user_id)
50
+
`);
51
+
}
52
+
53
+
async get(sessionId: string): Promise<SessionData | null> {
54
+
const result = await this.sqlite.execute({
55
+
sql: `SELECT session_id, user_id, handle, is_authenticated, data, created_at, expires_at, last_accessed_at
56
+
FROM sessions
57
+
WHERE session_id = ?`,
58
+
args: [sessionId]
59
+
});
60
+
61
+
if (result.rows.length === 0) return null;
62
+
63
+
return this.rowToSessionData(result.rows[0]);
64
+
}
65
+
66
+
async set(sessionId: string, data: SessionData): Promise<void> {
67
+
await this.sqlite.execute({
68
+
sql: `INSERT OR REPLACE INTO sessions
69
+
(session_id, user_id, handle, is_authenticated, data, created_at, expires_at, last_accessed_at)
70
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
71
+
args: [
72
+
sessionId,
73
+
data.userId,
74
+
data.handle ?? null,
75
+
data.isAuthenticated ? 1 : 0,
76
+
data.data ? JSON.stringify(data.data) : null,
77
+
data.createdAt,
78
+
data.expiresAt,
79
+
data.lastAccessedAt,
80
+
]
81
+
});
82
+
}
83
+
84
+
async update(sessionId: string, updates: Partial<SessionData>): Promise<boolean> {
85
+
const setParts: string[] = [];
86
+
const values: InValue[] = [];
87
+
88
+
if (updates.userId !== undefined) {
89
+
setParts.push("user_id = ?");
90
+
values.push(updates.userId);
91
+
}
92
+
93
+
if (updates.handle !== undefined) {
94
+
setParts.push("handle = ?");
95
+
values.push(updates.handle ?? null);
96
+
}
97
+
98
+
if (updates.isAuthenticated !== undefined) {
99
+
setParts.push("is_authenticated = ?");
100
+
values.push(updates.isAuthenticated ? 1 : 0);
101
+
}
102
+
103
+
if (updates.data !== undefined) {
104
+
setParts.push("data = ?");
105
+
values.push(updates.data ? JSON.stringify(updates.data) : null);
106
+
}
107
+
108
+
if (updates.expiresAt !== undefined) {
109
+
setParts.push("expires_at = ?");
110
+
values.push(updates.expiresAt);
111
+
}
112
+
113
+
if (updates.lastAccessedAt !== undefined) {
114
+
setParts.push("last_accessed_at = ?");
115
+
values.push(updates.lastAccessedAt);
116
+
}
117
+
118
+
if (setParts.length === 0) return false;
119
+
120
+
// Add sessionId as the last parameter for WHERE clause
121
+
values.push(sessionId);
122
+
123
+
const result = await this.sqlite.execute({
124
+
sql: `UPDATE sessions
125
+
SET ${setParts.join(", ")}
126
+
WHERE session_id = ?`,
127
+
args: values
128
+
});
129
+
130
+
return result.rowsAffected > 0;
131
+
}
132
+
133
+
async delete(sessionId: string): Promise<void> {
134
+
await this.sqlite.execute({
135
+
sql: "DELETE FROM sessions WHERE session_id = ?",
136
+
args: [sessionId]
137
+
});
138
+
}
139
+
140
+
async cleanup(expiresBeforeMs: number): Promise<number> {
141
+
const result = await this.sqlite.execute({
142
+
sql: "DELETE FROM sessions WHERE expires_at < ?",
143
+
args: [expiresBeforeMs]
144
+
});
145
+
return result.rowsAffected;
146
+
}
147
+
148
+
async exists(sessionId: string): Promise<boolean> {
149
+
const result = await this.sqlite.execute({
150
+
sql: "SELECT 1 FROM sessions WHERE session_id = ? LIMIT 1",
151
+
args: [sessionId]
152
+
});
153
+
return result.rows.length > 0;
154
+
}
155
+
156
+
private rowToSessionData(row: unknown[]): SessionData {
157
+
return {
158
+
sessionId: row[0] as string,
159
+
userId: row[1] as string,
160
+
handle: (row[2] as string | null) ?? undefined,
161
+
isAuthenticated: Boolean(row[3] as number),
162
+
data: row[4] ? JSON.parse(row[4] as string) : undefined,
163
+
createdAt: row[5] as number,
164
+
expiresAt: row[6] as number,
165
+
lastAccessedAt: row[7] as number,
166
+
};
167
+
}
168
+
169
+
// Val Town SQLite-specific methods
170
+
async getSessionsByUser(userId: string): Promise<SessionData[]> {
171
+
const result = await this.sqlite.execute({
172
+
sql: `SELECT session_id, user_id, handle, is_authenticated, data, created_at, expires_at, last_accessed_at
173
+
FROM sessions
174
+
WHERE user_id = ?`,
175
+
args: [userId]
176
+
});
177
+
178
+
return result.rows.map(row => this.rowToSessionData(row));
179
+
}
180
+
}