Skip to content

Commit 03603ee

Browse files
committed
WIP convex adapter
1 parent b43541b commit 03603ee

File tree

3 files changed

+304
-1
lines changed

3 files changed

+304
-1
lines changed

bun.lock

Lines changed: 57 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "keypal",
3-
"version": "0.0.00",
3+
"version": "0.0.2",
44
"description": "A TypeScript library for secure API key management with cryptographic hashing, expiration, scopes, and pluggable storage",
55
"type": "module",
66
"main": "./dist/index.cjs",
@@ -106,6 +106,7 @@
106106
"@prisma/client": "^6.18.0"
107107
},
108108
"dependencies": {
109+
"convex": "^1.28.0",
109110
"nanoid": "^5.1.6",
110111
"typebox": "^1.0.43"
111112
},

src/storage/convex.ts

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import type { ApiKeyMetadata, ApiKeyRecord } from "../types/api-key-types";
2+
import type {
3+
AuditLog,
4+
AuditLogQuery,
5+
AuditLogStats,
6+
} from "../types/audit-log-types";
7+
import type { Storage } from "../types/storage-types";
8+
9+
// WIP: This is a placeholder for the Convex storage adapter.
10+
11+
// Convex types - these would be imported from convex/server in a real implementation
12+
type ConvexCtx = any;
13+
type ConvexApi = any;
14+
15+
/**
16+
* Convex storage adapter for API keys
17+
*
18+
* **Setup Instructions:**
19+
*
20+
* 1. Create your Convex schema:
21+
* ```ts
22+
* // convex/schema.ts
23+
* import { defineSchema, defineTable } from "convex/server";
24+
* import { v } from "convex/values";
25+
*
26+
* export default defineSchema({
27+
* apiKeys: defineTable({
28+
* keyHash: v.string(),
29+
* metadata: v.any(),
30+
* })
31+
* .index("by_keyHash", ["keyHash"])
32+
* .index("by_owner", ["metadata.ownerId"]),
33+
*
34+
* auditLogs: defineTable({
35+
* keyId: v.string(),
36+
* ownerId: v.string(),
37+
* action: v.string(),
38+
* timestamp: v.string(),
39+
* data: v.optional(v.any()),
40+
* })
41+
* .index("by_keyId", ["keyId"])
42+
* .index("by_owner", ["ownerId"])
43+
* .index("by_timestamp", ["timestamp"]),
44+
* });
45+
* ```
46+
*
47+
* 2. Create Convex functions for your tables
48+
* (You'll need to implement mutations/queries for CRUD operations)
49+
*
50+
* 3. Use the adapter:
51+
* ```typescript
52+
* import { ConvexStore } from 'keypal/convex';
53+
* import { api } from './_generated/api';
54+
*
55+
* const store = new ConvexStore({
56+
* ctx, // Your Convex ctx (query or action)
57+
* api, // Your Convex api object
58+
* tableName: 'apiKeys',
59+
* logTableName: 'auditLogs',
60+
* });
61+
* ```
62+
*/
63+
export class ConvexStore implements Storage {
64+
private readonly ctx: ConvexCtx;
65+
private readonly api: ConvexApi;
66+
private readonly tableName: string;
67+
private readonly logTableName: string;
68+
69+
constructor(options: {
70+
ctx: ConvexCtx;
71+
api: ConvexApi;
72+
tableName?: string;
73+
logTableName?: string;
74+
}) {
75+
this.ctx = options.ctx;
76+
this.api = options.api;
77+
this.tableName = options.tableName ?? "apiKeys";
78+
this.logTableName = options.logTableName ?? "auditLogs";
79+
}
80+
81+
private toRecord(doc: any): ApiKeyRecord {
82+
return {
83+
id: doc._id,
84+
keyHash: doc.keyHash,
85+
metadata: doc.metadata as ApiKeyMetadata,
86+
};
87+
}
88+
89+
async save(record: ApiKeyRecord): Promise<void> {
90+
if (!("runMutation" in this.ctx)) {
91+
throw new Error("save requires an ActionCtx (runMutation)");
92+
}
93+
94+
const existing = await this.findById(record.id);
95+
if (existing) {
96+
throw new Error(`API key with id ${record.id} already exists`);
97+
}
98+
99+
await this.ctx.runMutation(this.api.storage.create, {
100+
table: this.tableName,
101+
data: {
102+
_id: record.id,
103+
keyHash: record.keyHash,
104+
metadata: record.metadata,
105+
},
106+
});
107+
}
108+
109+
async findByHash(keyHash: string): Promise<ApiKeyRecord | null> {
110+
const result = await this.ctx.runQuery(this.api.storage.findByHash, {
111+
table: this.tableName,
112+
keyHash,
113+
});
114+
115+
return result ? this.toRecord(result) : null;
116+
}
117+
118+
async findById(id: string): Promise<ApiKeyRecord | null> {
119+
const result = await this.ctx.runQuery(this.api.storage.findById, {
120+
table: this.tableName,
121+
id,
122+
});
123+
124+
return result ? this.toRecord(result) : null;
125+
}
126+
127+
async findByOwner(ownerId: string): Promise<ApiKeyRecord[]> {
128+
const results = await this.ctx.runQuery(this.api.storage.findByOwner, {
129+
table: this.tableName,
130+
ownerId,
131+
});
132+
133+
return results.map((doc: any) => this.toRecord(doc));
134+
}
135+
136+
async findByTags(tags: string[], ownerId?: string): Promise<ApiKeyRecord[]> {
137+
const results = await this.ctx.runQuery(this.api.storage.findByTags, {
138+
table: this.tableName,
139+
tags,
140+
ownerId,
141+
});
142+
143+
return results.map((doc: any) => this.toRecord(doc));
144+
}
145+
146+
async findByTag(tag: string, ownerId?: string): Promise<ApiKeyRecord[]> {
147+
return this.findByTags([tag], ownerId);
148+
}
149+
150+
async updateMetadata(
151+
id: string,
152+
metadata: Partial<ApiKeyMetadata>
153+
): Promise<void> {
154+
if (!("runMutation" in this.ctx)) {
155+
throw new Error("updateMetadata requires an ActionCtx (runMutation)");
156+
}
157+
158+
await this.ctx.runMutation(this.api.storage.updateMetadata, {
159+
table: this.tableName,
160+
id,
161+
metadata,
162+
});
163+
}
164+
165+
async delete(id: string): Promise<void> {
166+
if (!("runMutation" in this.ctx)) {
167+
throw new Error("delete requires an ActionCtx (runMutation)");
168+
}
169+
170+
await this.ctx.runMutation(this.api.storage.delete, {
171+
table: this.tableName,
172+
id,
173+
});
174+
}
175+
176+
async deleteByOwner(ownerId: string): Promise<void> {
177+
if (!("runMutation" in this.ctx)) {
178+
throw new Error("deleteByOwner requires an ActionCtx (runMutation)");
179+
}
180+
181+
await this.ctx.runMutation(this.api.storage.deleteByOwner, {
182+
table: this.tableName,
183+
ownerId,
184+
});
185+
}
186+
187+
async saveLog(log: AuditLog): Promise<void> {
188+
if (!("runMutation" in this.ctx)) {
189+
throw new Error("saveLog requires an ActionCtx (runMutation)");
190+
}
191+
192+
await this.ctx.runMutation(this.api.storage.createLog, {
193+
table: this.logTableName,
194+
data: {
195+
_id: log.id,
196+
keyId: log.keyId,
197+
ownerId: log.ownerId,
198+
action: log.action,
199+
timestamp: log.timestamp,
200+
data: log.data,
201+
},
202+
});
203+
}
204+
205+
async findLogs(query: AuditLogQuery): Promise<AuditLog[]> {
206+
const results = await this.ctx.runQuery(this.api.storage.findLogs, {
207+
table: this.logTableName,
208+
query,
209+
});
210+
211+
return results.map((doc: any) => ({
212+
id: doc._id,
213+
keyId: doc.keyId,
214+
ownerId: doc.ownerId,
215+
action: doc.action,
216+
timestamp: doc.timestamp,
217+
}));
218+
}
219+
220+
async countLogs(query: AuditLogQuery): Promise<number> {
221+
return await this.ctx.runQuery(this.api.storage.countLogs, {
222+
table: this.logTableName,
223+
query,
224+
});
225+
}
226+
227+
async deleteLogs(query: AuditLogQuery): Promise<number> {
228+
if (!("runMutation" in this.ctx)) {
229+
throw new Error("deleteLogs requires an ActionCtx (runMutation)");
230+
}
231+
232+
return await this.ctx.runMutation(this.api.storage.deleteLogs, {
233+
table: this.logTableName,
234+
query,
235+
});
236+
}
237+
238+
async getLogStats(ownerId: string): Promise<AuditLogStats> {
239+
return await this.ctx.runQuery(this.api.storage.getLogStats, {
240+
table: this.logTableName,
241+
ownerId,
242+
});
243+
}
244+
}
245+

0 commit comments

Comments
 (0)