@gaganref/convex-api-keys 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +419 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/crypto.d.ts +4 -0
- package/dist/client/crypto.d.ts.map +1 -0
- package/dist/client/crypto.js +48 -0
- package/dist/client/crypto.js.map +1 -0
- package/dist/client/errors.d.ts +32 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/errors.js +43 -0
- package/dist/client/errors.js.map +1 -0
- package/dist/client/index.d.ts +7 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +4 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/operations.d.ts +240 -0
- package/dist/client/operations.d.ts.map +1 -0
- package/dist/client/operations.js +700 -0
- package/dist/client/operations.js.map +1 -0
- package/dist/client/options.d.ts +79 -0
- package/dist/client/options.d.ts.map +1 -0
- package/dist/client/options.js +51 -0
- package/dist/client/options.js.map +1 -0
- package/dist/client/types.d.ts +269 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +24 -0
- package/dist/client/types.js.map +1 -0
- package/dist/component/_generated/api.d.ts +40 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +253 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/cleanup.d.ts +29 -0
- package/dist/component/cleanup.d.ts.map +1 -0
- package/dist/component/cleanup.js +70 -0
- package/dist/component/cleanup.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/crons.d.ts +3 -0
- package/dist/component/crons.d.ts.map +1 -0
- package/dist/component/crons.js +7 -0
- package/dist/component/crons.js.map +1 -0
- package/dist/component/lib.d.ts +323 -0
- package/dist/component/lib.d.ts.map +1 -0
- package/dist/component/lib.js +659 -0
- package/dist/component/lib.js.map +1 -0
- package/dist/component/schema.d.ts +82 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +38 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/sweep.d.ts +27 -0
- package/dist/component/sweep.d.ts.map +1 -0
- package/dist/component/sweep.js +94 -0
- package/dist/component/sweep.js.map +1 -0
- package/dist/shared.d.ts +11 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +11 -0
- package/dist/shared.js.map +1 -0
- package/package.json +116 -0
- package/src/client/__tests__/contracts.test.ts +109 -0
- package/src/client/__tests__/errors.test.ts +133 -0
- package/src/client/__tests__/hooks.test.ts +154 -0
- package/src/client/__tests__/operations.test.ts +742 -0
- package/src/client/__tests__/setup.test.ts +31 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/crypto.ts +64 -0
- package/src/client/errors.ts +67 -0
- package/src/client/index.ts +44 -0
- package/src/client/operations.ts +881 -0
- package/src/client/options.ts +146 -0
- package/src/client/types.ts +313 -0
- package/src/component/__tests__/cleanup.test.ts +472 -0
- package/src/component/__tests__/lib.test.ts +676 -0
- package/src/component/__tests__/setup.test.ts +11 -0
- package/src/component/_generated/api.ts +56 -0
- package/src/component/_generated/component.ts +300 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/cleanup.ts +85 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/crons.ts +20 -0
- package/src/component/lib.ts +843 -0
- package/src/component/schema.ts +49 -0
- package/src/component/sweep.ts +117 -0
- package/src/shared.ts +18 -0
- package/src/test.ts +18 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
import {
|
|
4
|
+
apiKeyStatusValidator,
|
|
5
|
+
metadataValidator,
|
|
6
|
+
permissionsValidator,
|
|
7
|
+
} from "../shared.js";
|
|
8
|
+
|
|
9
|
+
export const apiKeysFields = {
|
|
10
|
+
tokenHash: v.string(),
|
|
11
|
+
tokenPrefix: v.string(),
|
|
12
|
+
tokenLast4: v.string(),
|
|
13
|
+
namespace: v.optional(v.string()),
|
|
14
|
+
name: v.optional(v.string()),
|
|
15
|
+
permissions: v.optional(permissionsValidator),
|
|
16
|
+
metadata: v.optional(metadataValidator),
|
|
17
|
+
status: apiKeyStatusValidator,
|
|
18
|
+
expiresAt: v.optional(v.number()),
|
|
19
|
+
maxIdleMs: v.optional(v.number()),
|
|
20
|
+
lastUsedAt: v.number(),
|
|
21
|
+
revokedAt: v.optional(v.number()),
|
|
22
|
+
revocationReason: v.optional(v.string()),
|
|
23
|
+
replaces: v.optional(v.id("apiKeys")),
|
|
24
|
+
updatedAt: v.number(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const apiKeyEventsFields = {
|
|
28
|
+
keyId: v.id("apiKeys"),
|
|
29
|
+
namespace: v.optional(v.string()),
|
|
30
|
+
type: v.union(
|
|
31
|
+
v.literal("created"),
|
|
32
|
+
v.literal("revoked"),
|
|
33
|
+
v.literal("rotated"),
|
|
34
|
+
),
|
|
35
|
+
reason: v.optional(v.string()),
|
|
36
|
+
metadata: v.optional(metadataValidator),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default defineSchema({
|
|
40
|
+
apiKeys: defineTable(apiKeysFields)
|
|
41
|
+
.index("by_token_hash", ["tokenHash"])
|
|
42
|
+
.index("by_status", ["status"])
|
|
43
|
+
.index("by_namespace_and_status", ["namespace", "status"])
|
|
44
|
+
.index("by_revoked_at", ["revokedAt"]),
|
|
45
|
+
|
|
46
|
+
apiKeyEvents: defineTable(apiKeyEventsFields)
|
|
47
|
+
.index("by_key_id", ["keyId"])
|
|
48
|
+
.index("by_namespace", ["namespace"]),
|
|
49
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { internalMutation } from "./_generated/server.js";
|
|
2
|
+
import type { MutationCtx } from "./_generated/server.js";
|
|
3
|
+
import { internal } from "./_generated/api.js";
|
|
4
|
+
import { v } from "convex/values";
|
|
5
|
+
import type { Id } from "./_generated/dataModel.js";
|
|
6
|
+
|
|
7
|
+
const BATCH_SIZE = 100;
|
|
8
|
+
|
|
9
|
+
function isIdleExpired(
|
|
10
|
+
key: { maxIdleMs?: number; lastUsedAt: number },
|
|
11
|
+
now: number,
|
|
12
|
+
): boolean {
|
|
13
|
+
return key.maxIdleMs !== undefined && key.lastUsedAt + key.maxIdleMs < now;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Marks active keys past their absolute TTL as revoked.
|
|
18
|
+
*
|
|
19
|
+
* Uses cursor-based pagination to scan all active keys across multiple
|
|
20
|
+
* runs. Automatically reschedules itself until all pages are processed.
|
|
21
|
+
* Runs via the component's internal cron.
|
|
22
|
+
*/
|
|
23
|
+
export const sweepExpired = internalMutation({
|
|
24
|
+
args: {
|
|
25
|
+
cursor: v.optional(v.string()),
|
|
26
|
+
},
|
|
27
|
+
returns: v.object({
|
|
28
|
+
swept: v.number(),
|
|
29
|
+
isDone: v.boolean(),
|
|
30
|
+
}),
|
|
31
|
+
handler: async (ctx, args) => {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
|
|
34
|
+
const result = await ctx.db
|
|
35
|
+
.query("apiKeys")
|
|
36
|
+
.withIndex("by_status", (q) => q.eq("status", "active"))
|
|
37
|
+
.paginate({ numItems: BATCH_SIZE, cursor: args.cursor ?? null });
|
|
38
|
+
|
|
39
|
+
let swept = 0;
|
|
40
|
+
for (const key of result.page) {
|
|
41
|
+
if (key.expiresAt !== undefined && key.expiresAt < now) {
|
|
42
|
+
await markAsRevoked(ctx, key._id, key.namespace, now, "expired");
|
|
43
|
+
swept++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!result.isDone) {
|
|
48
|
+
await ctx.scheduler.runAfter(0, internal.sweep.sweepExpired, {
|
|
49
|
+
cursor: result.continueCursor,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { swept, isDone: result.isDone };
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Marks active keys past their idle timeout as revoked.
|
|
59
|
+
*
|
|
60
|
+
* Uses cursor-based pagination to scan all active keys across multiple
|
|
61
|
+
* runs. Automatically reschedules itself until all pages are processed.
|
|
62
|
+
* Runs via the component's internal cron.
|
|
63
|
+
*/
|
|
64
|
+
export const sweepIdleExpired = internalMutation({
|
|
65
|
+
args: {
|
|
66
|
+
cursor: v.optional(v.string()),
|
|
67
|
+
},
|
|
68
|
+
returns: v.object({
|
|
69
|
+
swept: v.number(),
|
|
70
|
+
isDone: v.boolean(),
|
|
71
|
+
}),
|
|
72
|
+
handler: async (ctx, args) => {
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
|
|
75
|
+
const result = await ctx.db
|
|
76
|
+
.query("apiKeys")
|
|
77
|
+
.withIndex("by_status", (q) => q.eq("status", "active"))
|
|
78
|
+
.paginate({ numItems: BATCH_SIZE, cursor: args.cursor ?? null });
|
|
79
|
+
|
|
80
|
+
let swept = 0;
|
|
81
|
+
for (const key of result.page) {
|
|
82
|
+
if (isIdleExpired(key, now)) {
|
|
83
|
+
await markAsRevoked(ctx, key._id, key.namespace, now, "idle_timeout");
|
|
84
|
+
swept++;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!result.isDone) {
|
|
89
|
+
await ctx.scheduler.runAfter(0, internal.sweep.sweepIdleExpired, {
|
|
90
|
+
cursor: result.continueCursor,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { swept, isDone: result.isDone };
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
async function markAsRevoked(
|
|
99
|
+
ctx: MutationCtx,
|
|
100
|
+
keyId: Id<"apiKeys">,
|
|
101
|
+
namespace: string | undefined,
|
|
102
|
+
now: number,
|
|
103
|
+
reason: string,
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
await ctx.db.patch(keyId, {
|
|
106
|
+
status: "revoked" as const,
|
|
107
|
+
revokedAt: now,
|
|
108
|
+
revocationReason: reason,
|
|
109
|
+
updatedAt: now,
|
|
110
|
+
});
|
|
111
|
+
await ctx.db.insert("apiKeyEvents", {
|
|
112
|
+
keyId,
|
|
113
|
+
namespace,
|
|
114
|
+
type: "revoked",
|
|
115
|
+
reason,
|
|
116
|
+
});
|
|
117
|
+
}
|
package/src/shared.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stored status of an API key.
|
|
5
|
+
*
|
|
6
|
+
* Note: "expired" and "idle_timeout" are computed at read time from
|
|
7
|
+
* `expiresAt` / `lastUsedAt + maxIdleMs`, not stored directly.
|
|
8
|
+
*/
|
|
9
|
+
export const apiKeyStatusValidator = v.union(
|
|
10
|
+
v.literal("active"),
|
|
11
|
+
v.literal("revoked"),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export const permissionsValidator = v.record(v.string(), v.array(v.string()));
|
|
15
|
+
|
|
16
|
+
export const metadataValidator = v.record(v.string(), v.any());
|
|
17
|
+
|
|
18
|
+
export type ApiKeyStatus = "active" | "revoked";
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
import type { TestConvex } from "convex-test";
|
|
3
|
+
import type { GenericSchema, SchemaDefinition } from "convex/server";
|
|
4
|
+
import schema from "./component/schema.js";
|
|
5
|
+
const modules = import.meta.glob("./component/**/*.ts");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register the component with the test convex instance.
|
|
9
|
+
* @param t - The test convex instance, e.g. from calling `convexTest`.
|
|
10
|
+
* @param name - The name of the component, as registered in convex.config.ts.
|
|
11
|
+
*/
|
|
12
|
+
export function register(
|
|
13
|
+
t: TestConvex<SchemaDefinition<GenericSchema, boolean>>,
|
|
14
|
+
name: string = "apiKeys",
|
|
15
|
+
) {
|
|
16
|
+
t.registerComponent(name, schema, modules);
|
|
17
|
+
}
|
|
18
|
+
export default { register, schema, modules };
|