@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,133 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { ApiKeys, ApiKeysClientError, isApiKeysClientError } from "../index.js";
|
|
3
|
+
import { normalizeApiKeysOptions } from "../options.js";
|
|
4
|
+
import { components, initConvexTest } from "./setup.test.js";
|
|
5
|
+
import type { RunQueryCtx } from "../types.js";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// error contracts
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
describe("error contracts", () => {
|
|
12
|
+
test("TOKEN_REQUIRED is an ApiKeysClientError", async () => {
|
|
13
|
+
const client = new ApiKeys(components.apiKeys, { logLevel: "none" });
|
|
14
|
+
const ctx: RunQueryCtx = {
|
|
15
|
+
runQuery: async () => {
|
|
16
|
+
throw new Error("should not be called");
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
await expect(client.validate(ctx, { token: " " })).rejects.toSatisfy(
|
|
21
|
+
(e: unknown) => isApiKeysClientError(e) && e.code === "TOKEN_REQUIRED",
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("OPERATION_FAILED wraps infrastructure errors with cause", async () => {
|
|
26
|
+
const client = new ApiKeys(components.apiKeys, { logLevel: "none" });
|
|
27
|
+
const ctx: RunQueryCtx = {
|
|
28
|
+
runQuery: async () => {
|
|
29
|
+
throw new Error("simulated Convex failure");
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const error = await client
|
|
34
|
+
.validate(ctx, { token: "ak_sometoken" })
|
|
35
|
+
.catch((e) => e);
|
|
36
|
+
expect(isApiKeysClientError(error)).toBe(true);
|
|
37
|
+
if (isApiKeysClientError(error)) {
|
|
38
|
+
expect(error.code).toBe("OPERATION_FAILED");
|
|
39
|
+
expect(error.cause).toBeInstanceOf(Error);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("ok:false auth decisions do not throw — they return", async () => {
|
|
44
|
+
const t = initConvexTest();
|
|
45
|
+
const client = new ApiKeys(components.apiKeys, { logLevel: "none" });
|
|
46
|
+
const ctx: RunQueryCtx = { runQuery: (q, a) => t.query(q, a) };
|
|
47
|
+
|
|
48
|
+
const result = await client.validate(ctx, { token: "ak_unknown_token" });
|
|
49
|
+
expect(result.ok).toBe(false);
|
|
50
|
+
if (!result.ok) {
|
|
51
|
+
expect(result.reason).toBe("not_found");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("infrastructure failure throws ApiKeysClientError", async () => {
|
|
56
|
+
const client = new ApiKeys(components.apiKeys, { logLevel: "none" });
|
|
57
|
+
const ctx: RunQueryCtx = {
|
|
58
|
+
runQuery: async () => {
|
|
59
|
+
throw new Error("db down");
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
await expect(
|
|
64
|
+
client.validate(ctx, { token: "ak_sometoken" }),
|
|
65
|
+
).rejects.toBeInstanceOf(ApiKeysClientError);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("INVALID_OPTIONS thrown at init time for bad config", () => {
|
|
69
|
+
expect(
|
|
70
|
+
() =>
|
|
71
|
+
new ApiKeys(components.apiKeys, {
|
|
72
|
+
keyDefaults: { prefix: "" },
|
|
73
|
+
}),
|
|
74
|
+
).toThrow();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// option normalization
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
describe("option normalization", () => {
|
|
83
|
+
test("normalizes defaults", () => {
|
|
84
|
+
const normalized = normalizeApiKeysOptions({});
|
|
85
|
+
|
|
86
|
+
expect(normalized.keyDefaults.prefix).toBe("ak_");
|
|
87
|
+
expect(normalized.keyDefaults.keyLengthBytes).toBe(32);
|
|
88
|
+
expect(normalized.keyDefaults.ttlMs).toBe(null);
|
|
89
|
+
expect(normalized.logLevel).toBe("warn");
|
|
90
|
+
expect(normalized.permissionDefaults).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("normalizes logLevel option", () => {
|
|
94
|
+
const normalized = normalizeApiKeysOptions({ logLevel: "debug" });
|
|
95
|
+
expect(normalized.logLevel).toBe("debug");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("normalizes permissionDefaults", () => {
|
|
99
|
+
const normalized = normalizeApiKeysOptions({
|
|
100
|
+
permissionDefaults: { beacon: ["events:read"] },
|
|
101
|
+
});
|
|
102
|
+
expect(normalized.permissionDefaults).toEqual({
|
|
103
|
+
beacon: ["events:read"],
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("throws for empty prefix", () => {
|
|
108
|
+
expect(() =>
|
|
109
|
+
normalizeApiKeysOptions({ keyDefaults: { prefix: "" } }),
|
|
110
|
+
).toThrow(/must not be empty/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("throws for prefix exceeding max length", () => {
|
|
114
|
+
expect(() =>
|
|
115
|
+
normalizeApiKeysOptions({ keyDefaults: { prefix: "a".repeat(33) } }),
|
|
116
|
+
).toThrow(/max allowed length/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("throws typed INVALID_OPTIONS for bad initialization", () => {
|
|
120
|
+
try {
|
|
121
|
+
new ApiKeys(components.apiKeys, {
|
|
122
|
+
keyDefaults: { prefix: "" },
|
|
123
|
+
});
|
|
124
|
+
throw new Error("expected constructor to throw");
|
|
125
|
+
} catch (error) {
|
|
126
|
+
expect(error).toBeInstanceOf(ApiKeysClientError);
|
|
127
|
+
expect(isApiKeysClientError(error)).toBe(true);
|
|
128
|
+
if (isApiKeysClientError(error)) {
|
|
129
|
+
expect(error.code).toBe("INVALID_OPTIONS");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import type { FunctionReference } from "convex/server";
|
|
3
|
+
import { ApiKeys } from "../index.js";
|
|
4
|
+
import { components, initConvexTest } from "./setup.test.js";
|
|
5
|
+
import type { OnInvalidateHookPayload } from "../types.js";
|
|
6
|
+
import type { RunMutationCtx, RunQueryCtx } from "../types.js";
|
|
7
|
+
|
|
8
|
+
describe("hooks", () => {
|
|
9
|
+
test("runs onInvalidate hook after invalidate and refresh", async () => {
|
|
10
|
+
const t = initConvexTest();
|
|
11
|
+
const hookRef = {} as unknown as FunctionReference<
|
|
12
|
+
"mutation",
|
|
13
|
+
"internal",
|
|
14
|
+
{ event: OnInvalidateHookPayload },
|
|
15
|
+
null
|
|
16
|
+
>;
|
|
17
|
+
const hookCalls: Array<OnInvalidateHookPayload> = [];
|
|
18
|
+
|
|
19
|
+
const client = new ApiKeys<{ namespace: string }>(
|
|
20
|
+
components.apiKeys,
|
|
21
|
+
{},
|
|
22
|
+
).withHooks({ onInvalidate: hookRef });
|
|
23
|
+
|
|
24
|
+
const mutationCtx: RunMutationCtx = {
|
|
25
|
+
runMutation: (mutation, args) => {
|
|
26
|
+
if (mutation === hookRef) {
|
|
27
|
+
hookCalls.push(args.event);
|
|
28
|
+
return Promise.resolve(null) as never;
|
|
29
|
+
}
|
|
30
|
+
return t.mutation(mutation, args);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const created = await client.create(mutationCtx, {
|
|
35
|
+
namespace: "team_alpha",
|
|
36
|
+
name: "hook target",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await client.invalidate(mutationCtx, {
|
|
40
|
+
keyId: created.keyId,
|
|
41
|
+
reason: "manual",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const created2 = await client.create(mutationCtx, {
|
|
45
|
+
namespace: "team_alpha",
|
|
46
|
+
name: "hook rotate",
|
|
47
|
+
});
|
|
48
|
+
await client.refresh(mutationCtx, {
|
|
49
|
+
keyId: created2.keyId,
|
|
50
|
+
reason: "rotate",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(hookCalls.some((call) => call.trigger === "invalidate")).toBe(true);
|
|
54
|
+
expect(hookCalls.some((call) => call.trigger === "refresh")).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("client without onInvalidate does not fire hooks", async () => {
|
|
58
|
+
const t = initConvexTest();
|
|
59
|
+
const hookRef = {} as unknown as FunctionReference<
|
|
60
|
+
"mutation",
|
|
61
|
+
"internal",
|
|
62
|
+
{ event: OnInvalidateHookPayload },
|
|
63
|
+
null
|
|
64
|
+
>;
|
|
65
|
+
const hookCalls: Array<OnInvalidateHookPayload> = [];
|
|
66
|
+
|
|
67
|
+
const baseClient = new ApiKeys<{ namespace: string }>(
|
|
68
|
+
components.apiKeys,
|
|
69
|
+
{},
|
|
70
|
+
);
|
|
71
|
+
const hookedClient = new ApiKeys<{ namespace: string }>(
|
|
72
|
+
components.apiKeys,
|
|
73
|
+
{},
|
|
74
|
+
).withHooks({ onInvalidate: hookRef });
|
|
75
|
+
|
|
76
|
+
const mutationCtx: RunMutationCtx = {
|
|
77
|
+
runMutation: (mutation, args) => {
|
|
78
|
+
if (mutation === hookRef) {
|
|
79
|
+
hookCalls.push(args.event);
|
|
80
|
+
return Promise.resolve(null) as never;
|
|
81
|
+
}
|
|
82
|
+
return t.mutation(mutation, args);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const createdByBase = await baseClient.create(mutationCtx, {
|
|
87
|
+
namespace: "team_alpha",
|
|
88
|
+
name: "base client key",
|
|
89
|
+
});
|
|
90
|
+
await baseClient.invalidate(mutationCtx, {
|
|
91
|
+
keyId: createdByBase.keyId,
|
|
92
|
+
reason: "base",
|
|
93
|
+
});
|
|
94
|
+
expect(hookCalls).toHaveLength(0);
|
|
95
|
+
|
|
96
|
+
const createdByHooked = await hookedClient.create(mutationCtx, {
|
|
97
|
+
namespace: "team_alpha",
|
|
98
|
+
name: "hooked client key",
|
|
99
|
+
});
|
|
100
|
+
await hookedClient.invalidate(mutationCtx, {
|
|
101
|
+
keyId: createdByHooked.keyId,
|
|
102
|
+
reason: "hooked",
|
|
103
|
+
});
|
|
104
|
+
expect(hookCalls.some((call) => call.trigger === "invalidate")).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("swallows onInvalidate hook failures", async () => {
|
|
108
|
+
const t = initConvexTest();
|
|
109
|
+
const hookRef = {} as unknown as FunctionReference<
|
|
110
|
+
"mutation",
|
|
111
|
+
"internal",
|
|
112
|
+
{ event: OnInvalidateHookPayload },
|
|
113
|
+
null
|
|
114
|
+
>;
|
|
115
|
+
|
|
116
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
117
|
+
|
|
118
|
+
const client = new ApiKeys<{ namespace: string }>(
|
|
119
|
+
components.apiKeys,
|
|
120
|
+
{},
|
|
121
|
+
).withHooks({ onInvalidate: hookRef });
|
|
122
|
+
|
|
123
|
+
const mutationCtx: RunMutationCtx = {
|
|
124
|
+
runMutation: (mutation, args) => {
|
|
125
|
+
if (mutation === hookRef) {
|
|
126
|
+
throw new Error("hook boom");
|
|
127
|
+
}
|
|
128
|
+
return t.mutation(mutation, args);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
const queryCtx: RunQueryCtx = {
|
|
132
|
+
runQuery: (query, args) => t.query(query, args),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const created = await client.create(mutationCtx, {
|
|
136
|
+
namespace: "team_alpha",
|
|
137
|
+
name: "hook failure",
|
|
138
|
+
});
|
|
139
|
+
const result = await client.invalidate(mutationCtx, {
|
|
140
|
+
keyId: created.keyId,
|
|
141
|
+
reason: "manual",
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(result.ok).toBe(true);
|
|
145
|
+
expect(warnSpy).toHaveBeenCalledWith("[api-keys:system]", {
|
|
146
|
+
message: "onInvalidate hook failed",
|
|
147
|
+
error: expect.any(String),
|
|
148
|
+
});
|
|
149
|
+
warnSpy.mockRestore();
|
|
150
|
+
|
|
151
|
+
const validate = await client.validate(queryCtx, { token: created.token });
|
|
152
|
+
expect(validate).toEqual({ ok: false, reason: "revoked" });
|
|
153
|
+
});
|
|
154
|
+
});
|