@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,472 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test, vi } from "vitest";
|
|
4
|
+
import { api, internal } from "../_generated/api.js";
|
|
5
|
+
import type { Id } from "../_generated/dataModel.js";
|
|
6
|
+
import { initConvexTest } from "./setup.test.js";
|
|
7
|
+
|
|
8
|
+
// Helper: create a key via the component's create mutation.
|
|
9
|
+
// Default expiresAt is set to FAR_FUTURE so it doesn't interfere with
|
|
10
|
+
// sweep queries. Tests override to control behavior.
|
|
11
|
+
const FAR_FUTURE = Date.now() + 365 * 86_400_000;
|
|
12
|
+
|
|
13
|
+
async function createKey(
|
|
14
|
+
t: ReturnType<typeof initConvexTest>,
|
|
15
|
+
overrides: {
|
|
16
|
+
tokenHash: string;
|
|
17
|
+
expiresAt?: number;
|
|
18
|
+
maxIdleMs?: number;
|
|
19
|
+
lastUsedAt?: number;
|
|
20
|
+
namespace?: string;
|
|
21
|
+
},
|
|
22
|
+
) {
|
|
23
|
+
const result = await t.mutation(api.lib.create, {
|
|
24
|
+
tokenHash: overrides.tokenHash,
|
|
25
|
+
tokenPrefix: "ak_",
|
|
26
|
+
tokenLast4: "test",
|
|
27
|
+
namespace: overrides.namespace ?? "cleanup-ns",
|
|
28
|
+
expiresAt: overrides.expiresAt ?? FAR_FUTURE,
|
|
29
|
+
maxIdleMs: overrides.maxIdleMs,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// If lastUsedAt is provided, patch it directly (simulates a past touch)
|
|
33
|
+
if (overrides.lastUsedAt !== undefined) {
|
|
34
|
+
await t.run(async (ctx) => {
|
|
35
|
+
await ctx.db.patch(result.keyId, { lastUsedAt: overrides.lastUsedAt });
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function revokeKey(
|
|
43
|
+
t: ReturnType<typeof initConvexTest>,
|
|
44
|
+
keyId: Id<"apiKeys">,
|
|
45
|
+
now?: number,
|
|
46
|
+
) {
|
|
47
|
+
return t.mutation(api.lib.invalidate, {
|
|
48
|
+
keyId,
|
|
49
|
+
now: now ?? Date.now(),
|
|
50
|
+
logLevel: "none",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function getKey(
|
|
55
|
+
t: ReturnType<typeof initConvexTest>,
|
|
56
|
+
keyId: Id<"apiKeys">,
|
|
57
|
+
) {
|
|
58
|
+
return t.query(api.lib.getKey, { keyId, now: Date.now() });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function listKeys(t: ReturnType<typeof initConvexTest>) {
|
|
62
|
+
return t.query(api.lib.listKeys, {
|
|
63
|
+
namespace: "cleanup-ns",
|
|
64
|
+
now: Date.now(),
|
|
65
|
+
paginationOpts: { numItems: 50, cursor: null },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ONE_HOUR = 3_600_000;
|
|
70
|
+
const ONE_DAY = 86_400_000;
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// sweepExpired: marks active keys past absolute TTL as revoked
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
describe("sweepExpired", () => {
|
|
77
|
+
test("does not sweep active keys with future expiry", async () => {
|
|
78
|
+
const t = initConvexTest();
|
|
79
|
+
await createKey(t, { tokenHash: "active_key" });
|
|
80
|
+
|
|
81
|
+
const result = await t.mutation(internal.sweep.sweepExpired, {});
|
|
82
|
+
|
|
83
|
+
expect(result.swept).toBe(0);
|
|
84
|
+
expect(result.isDone).toBe(true);
|
|
85
|
+
|
|
86
|
+
const keys = await listKeys(t);
|
|
87
|
+
expect(keys.page).toHaveLength(1);
|
|
88
|
+
expect(keys.page[0].status).toBe("active");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("sweeps time-expired keys to revoked", async () => {
|
|
92
|
+
const t = initConvexTest();
|
|
93
|
+
const past = Date.now() - ONE_DAY * 2;
|
|
94
|
+
|
|
95
|
+
const created = await createKey(t, {
|
|
96
|
+
tokenHash: "expired_key",
|
|
97
|
+
expiresAt: past,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const result = await t.mutation(internal.sweep.sweepExpired, {});
|
|
101
|
+
|
|
102
|
+
expect(result.swept).toBe(1);
|
|
103
|
+
expect(result.isDone).toBe(true);
|
|
104
|
+
|
|
105
|
+
const key = await getKey(t, created.keyId);
|
|
106
|
+
expect(key.ok).toBe(true);
|
|
107
|
+
if (key.ok) {
|
|
108
|
+
expect(key.status).toBe("revoked");
|
|
109
|
+
expect(key.revocationReason).toBe("expired");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("records audit event on sweep", async () => {
|
|
114
|
+
const t = initConvexTest();
|
|
115
|
+
const past = Date.now() - ONE_DAY * 2;
|
|
116
|
+
|
|
117
|
+
const created = await createKey(t, {
|
|
118
|
+
tokenHash: "audit_key",
|
|
119
|
+
expiresAt: past,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await t.mutation(internal.sweep.sweepExpired, {});
|
|
123
|
+
|
|
124
|
+
const events = await t.query(api.lib.listKeyEvents, {
|
|
125
|
+
keyId: created.keyId,
|
|
126
|
+
paginationOpts: { numItems: 10, cursor: null },
|
|
127
|
+
});
|
|
128
|
+
const types = events.page.map((e: { type: string }) => e.type);
|
|
129
|
+
expect(types).toContain("revoked");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("does not re-sweep already-revoked keys", async () => {
|
|
133
|
+
const t = initConvexTest();
|
|
134
|
+
const past = Date.now() - ONE_DAY * 2;
|
|
135
|
+
|
|
136
|
+
await createKey(t, {
|
|
137
|
+
tokenHash: "no_resweep",
|
|
138
|
+
expiresAt: past,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const first = await t.mutation(internal.sweep.sweepExpired, {});
|
|
142
|
+
expect(first.swept).toBe(1);
|
|
143
|
+
|
|
144
|
+
const second = await t.mutation(internal.sweep.sweepExpired, {});
|
|
145
|
+
expect(second.swept).toBe(0);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("does not sweep idle-expired keys", async () => {
|
|
149
|
+
const t = initConvexTest();
|
|
150
|
+
const past = Date.now() - ONE_DAY * 2;
|
|
151
|
+
|
|
152
|
+
await createKey(t, {
|
|
153
|
+
tokenHash: "idle_only",
|
|
154
|
+
maxIdleMs: ONE_HOUR,
|
|
155
|
+
lastUsedAt: past,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const result = await t.mutation(internal.sweep.sweepExpired, {});
|
|
159
|
+
expect(result.swept).toBe(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("reaches later pages when first page has no expired keys", async () => {
|
|
163
|
+
vi.useFakeTimers();
|
|
164
|
+
const t = initConvexTest();
|
|
165
|
+
const past = Date.now() - ONE_DAY * 2;
|
|
166
|
+
|
|
167
|
+
// First 100 healthy active keys (fill first page)
|
|
168
|
+
for (let i = 0; i < 100; i++) {
|
|
169
|
+
await createKey(t, { tokenHash: `healthy_exp_${i}` });
|
|
170
|
+
}
|
|
171
|
+
// 5 expired keys on later pages
|
|
172
|
+
for (let i = 0; i < 5; i++) {
|
|
173
|
+
await createKey(t, { tokenHash: `expired_late_${i}`, expiresAt: past });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const first = await t.mutation(internal.sweep.sweepExpired, {});
|
|
177
|
+
expect(first.swept).toBe(0);
|
|
178
|
+
expect(first.isDone).toBe(false);
|
|
179
|
+
|
|
180
|
+
await t.finishAllScheduledFunctions(() => vi.runAllTimers());
|
|
181
|
+
|
|
182
|
+
const all = await t.run((ctx) => ctx.db.query("apiKeys").collect());
|
|
183
|
+
const revokedExpired = all.filter(
|
|
184
|
+
(k) => k.status === "revoked" && k.revocationReason === "expired",
|
|
185
|
+
);
|
|
186
|
+
expect(revokedExpired).toHaveLength(5);
|
|
187
|
+
|
|
188
|
+
vi.useRealTimers();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// sweepIdleExpired: marks active keys past idle timeout as revoked
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
describe("sweepIdleExpired", () => {
|
|
197
|
+
test("does not sweep active keys with future idle expiry", async () => {
|
|
198
|
+
const t = initConvexTest();
|
|
199
|
+
await createKey(t, { tokenHash: "active_idle", maxIdleMs: ONE_HOUR });
|
|
200
|
+
|
|
201
|
+
const result = await t.mutation(internal.sweep.sweepIdleExpired, {});
|
|
202
|
+
|
|
203
|
+
expect(result.swept).toBe(0);
|
|
204
|
+
expect(result.isDone).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("sweeps idle-expired keys to revoked", async () => {
|
|
208
|
+
const t = initConvexTest();
|
|
209
|
+
const past = Date.now() - ONE_DAY * 2;
|
|
210
|
+
|
|
211
|
+
const created = await createKey(t, {
|
|
212
|
+
tokenHash: "idle_key",
|
|
213
|
+
maxIdleMs: ONE_HOUR,
|
|
214
|
+
lastUsedAt: past,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const result = await t.mutation(internal.sweep.sweepIdleExpired, {});
|
|
218
|
+
|
|
219
|
+
expect(result.swept).toBe(1);
|
|
220
|
+
expect(result.isDone).toBe(true);
|
|
221
|
+
|
|
222
|
+
const key = await getKey(t, created.keyId);
|
|
223
|
+
expect(key.ok).toBe(true);
|
|
224
|
+
if (key.ok) {
|
|
225
|
+
expect(key.status).toBe("revoked");
|
|
226
|
+
expect(key.revocationReason).toBe("idle_timeout");
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("does not re-sweep already-revoked keys", async () => {
|
|
231
|
+
const t = initConvexTest();
|
|
232
|
+
const past = Date.now() - ONE_DAY * 2;
|
|
233
|
+
|
|
234
|
+
await createKey(t, {
|
|
235
|
+
tokenHash: "idle_no_resweep",
|
|
236
|
+
maxIdleMs: ONE_HOUR,
|
|
237
|
+
lastUsedAt: past,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const first = await t.mutation(internal.sweep.sweepIdleExpired, {});
|
|
241
|
+
expect(first.swept).toBe(1);
|
|
242
|
+
|
|
243
|
+
const second = await t.mutation(internal.sweep.sweepIdleExpired, {});
|
|
244
|
+
expect(second.swept).toBe(0);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("does not sweep time-expired keys", async () => {
|
|
248
|
+
const t = initConvexTest();
|
|
249
|
+
const past = Date.now() - ONE_DAY * 2;
|
|
250
|
+
|
|
251
|
+
await createKey(t, {
|
|
252
|
+
tokenHash: "expired_only",
|
|
253
|
+
expiresAt: past,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const result = await t.mutation(internal.sweep.sweepIdleExpired, {});
|
|
257
|
+
expect(result.swept).toBe(0);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("reaches later pages when first page has no idle-expired keys", async () => {
|
|
261
|
+
vi.useFakeTimers();
|
|
262
|
+
const t = initConvexTest();
|
|
263
|
+
const past = Date.now() - ONE_DAY * 2;
|
|
264
|
+
|
|
265
|
+
// First 100 healthy active keys (no idle timeout)
|
|
266
|
+
for (let i = 0; i < 100; i++) {
|
|
267
|
+
await createKey(t, { tokenHash: `healthy_idle_${i}` });
|
|
268
|
+
}
|
|
269
|
+
// 5 idle-expired keys on later pages
|
|
270
|
+
for (let i = 0; i < 5; i++) {
|
|
271
|
+
await createKey(t, {
|
|
272
|
+
tokenHash: `idle_late_${i}`,
|
|
273
|
+
maxIdleMs: ONE_HOUR,
|
|
274
|
+
lastUsedAt: past,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const first = await t.mutation(internal.sweep.sweepIdleExpired, {});
|
|
279
|
+
expect(first.swept).toBe(0);
|
|
280
|
+
expect(first.isDone).toBe(false);
|
|
281
|
+
|
|
282
|
+
await t.finishAllScheduledFunctions(() => vi.runAllTimers());
|
|
283
|
+
|
|
284
|
+
const all = await t.run((ctx) => ctx.db.query("apiKeys").collect());
|
|
285
|
+
const revokedIdle = all.filter(
|
|
286
|
+
(k) => k.status === "revoked" && k.revocationReason === "idle_timeout",
|
|
287
|
+
);
|
|
288
|
+
expect(revokedIdle).toHaveLength(5);
|
|
289
|
+
|
|
290
|
+
vi.useRealTimers();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// cleanup (public): hard-deletes revoked keys past retention
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
describe("cleanup", () => {
|
|
299
|
+
test("throws for non-positive retentionMs", async () => {
|
|
300
|
+
const t = initConvexTest();
|
|
301
|
+
await expect(
|
|
302
|
+
t.mutation(api.cleanup.cleanupExpired, { retentionMs: 0 }),
|
|
303
|
+
).rejects.toMatchObject({
|
|
304
|
+
data: expect.stringContaining('"code":"invalid_argument"'),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
await expect(
|
|
308
|
+
t.mutation(api.cleanup.cleanupExpired, { retentionMs: -1000 }),
|
|
309
|
+
).rejects.toMatchObject({
|
|
310
|
+
data: expect.stringContaining('"code":"invalid_argument"'),
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("returns zero counts when no keys exist", async () => {
|
|
315
|
+
const t = initConvexTest();
|
|
316
|
+
const result = await t.mutation(api.cleanup.cleanupExpired, {
|
|
317
|
+
retentionMs: ONE_DAY,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
expect(result).toEqual({ deleted: 0, isDone: true });
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("does not delete active (non-revoked) keys", async () => {
|
|
324
|
+
const t = initConvexTest();
|
|
325
|
+
await createKey(t, { tokenHash: "active_key" });
|
|
326
|
+
|
|
327
|
+
const result = await t.mutation(api.cleanup.cleanupExpired, {
|
|
328
|
+
retentionMs: ONE_HOUR,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
expect(result.deleted).toBe(0);
|
|
332
|
+
expect(result.isDone).toBe(true);
|
|
333
|
+
|
|
334
|
+
const keys = await listKeys(t);
|
|
335
|
+
expect(keys.page).toHaveLength(1);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("deletes revoked keys past retention", async () => {
|
|
339
|
+
const t = initConvexTest();
|
|
340
|
+
const past = Date.now() - ONE_DAY * 2;
|
|
341
|
+
|
|
342
|
+
const created = await createKey(t, { tokenHash: "revoked_key" });
|
|
343
|
+
await revokeKey(t, created.keyId, past);
|
|
344
|
+
|
|
345
|
+
const result = await t.mutation(api.cleanup.cleanupExpired, {
|
|
346
|
+
retentionMs: ONE_HOUR,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
expect(result.deleted).toBe(1);
|
|
350
|
+
expect(result.isDone).toBe(true);
|
|
351
|
+
|
|
352
|
+
const keys = await listKeys(t);
|
|
353
|
+
expect(keys.page).toHaveLength(0);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("does not delete revoked keys still within retention window", async () => {
|
|
357
|
+
const t = initConvexTest();
|
|
358
|
+
|
|
359
|
+
const created = await createKey(t, { tokenHash: "recent_revoked" });
|
|
360
|
+
await revokeKey(t, created.keyId);
|
|
361
|
+
|
|
362
|
+
const result = await t.mutation(api.cleanup.cleanupExpired, {
|
|
363
|
+
retentionMs: ONE_DAY * 30,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
expect(result.deleted).toBe(0);
|
|
367
|
+
expect(result.isDone).toBe(true);
|
|
368
|
+
|
|
369
|
+
const keys = await listKeys(t);
|
|
370
|
+
expect(keys.page).toHaveLength(1);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("deletes associated audit events alongside key", async () => {
|
|
374
|
+
const t = initConvexTest();
|
|
375
|
+
const past = Date.now() - ONE_DAY * 2;
|
|
376
|
+
|
|
377
|
+
const created = await createKey(t, { tokenHash: "events_key" });
|
|
378
|
+
await revokeKey(t, created.keyId, past);
|
|
379
|
+
|
|
380
|
+
const eventsBefore = await t.query(api.lib.listKeyEvents, {
|
|
381
|
+
keyId: created.keyId,
|
|
382
|
+
paginationOpts: { numItems: 10, cursor: null },
|
|
383
|
+
});
|
|
384
|
+
expect(eventsBefore.page.length).toBeGreaterThan(0);
|
|
385
|
+
|
|
386
|
+
await t.mutation(api.cleanup.cleanupExpired, {
|
|
387
|
+
retentionMs: ONE_HOUR,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const eventsAfter = await t.query(api.lib.listKeyEvents, {
|
|
391
|
+
keyId: created.keyId,
|
|
392
|
+
paginationOpts: { numItems: 10, cursor: null },
|
|
393
|
+
});
|
|
394
|
+
expect(eventsAfter.page).toHaveLength(0);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
// Full lifecycle: sweep then cleanup
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
describe("sweep + cleanup lifecycle", () => {
|
|
403
|
+
test("expired key is swept then retained until cleanup window passes", async () => {
|
|
404
|
+
const t = initConvexTest();
|
|
405
|
+
const past = Date.now() - ONE_DAY * 2;
|
|
406
|
+
|
|
407
|
+
const created = await createKey(t, {
|
|
408
|
+
tokenHash: "lifecycle_key",
|
|
409
|
+
expiresAt: past,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Sweep marks it as revoked
|
|
413
|
+
const sweep = await t.mutation(internal.sweep.sweepExpired, {});
|
|
414
|
+
expect(sweep.swept).toBe(1);
|
|
415
|
+
|
|
416
|
+
const key = await getKey(t, created.keyId);
|
|
417
|
+
expect(key.ok).toBe(true);
|
|
418
|
+
if (key.ok) {
|
|
419
|
+
expect(key.status).toBe("revoked");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Cleanup with large retention — key was just revoked, not past retention
|
|
423
|
+
const cleanup = await t.mutation(api.cleanup.cleanupExpired, {
|
|
424
|
+
retentionMs: ONE_DAY * 365,
|
|
425
|
+
});
|
|
426
|
+
expect(cleanup.deleted).toBe(0);
|
|
427
|
+
|
|
428
|
+
// Key still exists
|
|
429
|
+
const keys = await listKeys(t);
|
|
430
|
+
expect(keys.page).toHaveLength(1);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("mixed: both sweeps mark their keys, cleanup deletes old revoked", async () => {
|
|
434
|
+
const t = initConvexTest();
|
|
435
|
+
const past = Date.now() - ONE_DAY * 2;
|
|
436
|
+
|
|
437
|
+
// Expired key — will be swept by sweepExpired
|
|
438
|
+
await createKey(t, {
|
|
439
|
+
tokenHash: "mix_expired",
|
|
440
|
+
expiresAt: past,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Idle key — will be swept by sweepIdleExpired
|
|
444
|
+
await createKey(t, {
|
|
445
|
+
tokenHash: "mix_idle",
|
|
446
|
+
maxIdleMs: ONE_HOUR,
|
|
447
|
+
lastUsedAt: past,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Manually revoked key — already revoked, will be deleted by cleanup
|
|
451
|
+
const revokable = await createKey(t, { tokenHash: "mix_revoked" });
|
|
452
|
+
await revokeKey(t, revokable.keyId, past);
|
|
453
|
+
|
|
454
|
+
// Run both sweeps
|
|
455
|
+
const expiredSweep = await t.mutation(internal.sweep.sweepExpired, {});
|
|
456
|
+
expect(expiredSweep.swept).toBe(1);
|
|
457
|
+
|
|
458
|
+
const idleSweep = await t.mutation(internal.sweep.sweepIdleExpired, {});
|
|
459
|
+
expect(idleSweep.swept).toBe(1);
|
|
460
|
+
|
|
461
|
+
// Cleanup — only the manually revoked key (revokedAt=past) is old enough
|
|
462
|
+
const cleanup = await t.mutation(api.cleanup.cleanupExpired, {
|
|
463
|
+
retentionMs: ONE_HOUR,
|
|
464
|
+
});
|
|
465
|
+
expect(cleanup.deleted).toBe(1);
|
|
466
|
+
expect(cleanup.isDone).toBe(true);
|
|
467
|
+
|
|
468
|
+
// Two swept keys remain (revoked but within retention)
|
|
469
|
+
const keys = await listKeys(t);
|
|
470
|
+
expect(keys.page).toHaveLength(2);
|
|
471
|
+
});
|
|
472
|
+
});
|