@convex-dev/workos-authkit 0.2.9 → 0.3.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/README.md +14 -5
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +3 -2
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +0 -1
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +0 -2
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/backfill.d.ts +6 -6
- package/dist/component/backfill.d.ts.map +1 -1
- package/dist/component/backfill.js +12 -5
- package/dist/component/backfill.js.map +1 -1
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/convex.config.js +0 -2
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/lib.d.ts +7 -27
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +91 -112
- package/dist/component/lib.js.map +1 -1
- package/dist/component/schema.d.ts +13 -4
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +7 -14
- package/dist/component/schema.js.map +1 -1
- package/dist/validators.d.ts +30 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +17 -0
- package/dist/validators.js.map +1 -0
- package/package.json +7 -7
- package/src/client/index.test.ts +18 -0
- package/src/client/index.ts +10 -2
- package/src/component/_generated/api.ts +0 -1
- package/src/component/_generated/component.ts +0 -2
- package/src/component/backfill.test.ts +19 -11
- package/src/component/backfill.ts +25 -18
- package/src/component/convex.config.ts +0 -2
- package/src/component/lib.test.ts +248 -11
- package/src/component/lib.ts +95 -124
- package/src/component/schema.ts +7 -14
- package/src/test.ts +4 -3
- package/src/validators.ts +17 -0
package/src/component/lib.ts
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
|
+
import { type Event as WorkOSEvent } from "@workos-inc/node";
|
|
2
|
+
import { withoutSystemFields } from "convex-helpers";
|
|
3
|
+
import { parse } from "convex-helpers/validators";
|
|
4
|
+
import type { FunctionHandle } from "convex/server";
|
|
1
5
|
import { type Infer, v } from "convex/values";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
internalMutation,
|
|
5
|
-
internalQuery,
|
|
6
|
-
mutation,
|
|
7
|
-
query,
|
|
8
|
-
} from "./_generated/server.js";
|
|
6
|
+
import { vUser } from "../validators.js";
|
|
7
|
+
import type { Doc } from "./_generated/dataModel.js";
|
|
9
8
|
import type { MutationCtx } from "./_generated/server.js";
|
|
10
|
-
import {
|
|
11
|
-
import { omit, withoutSystemFields } from "convex-helpers";
|
|
12
|
-
import { WorkOS, type Event as WorkOSEvent } from "@workos-inc/node";
|
|
13
|
-
import type { FunctionHandle } from "convex/server";
|
|
14
|
-
import { Workpool } from "@convex-dev/workpool";
|
|
15
|
-
import schema from "./schema.js";
|
|
16
|
-
import { parse } from "convex-helpers/validators";
|
|
17
|
-
|
|
18
|
-
const eventWorkpool = new Workpool(components.eventWorkpool, {
|
|
19
|
-
maxParallelism: 1,
|
|
20
|
-
});
|
|
9
|
+
import { mutation, query } from "./_generated/server.js";
|
|
21
10
|
|
|
22
11
|
export const vEvent = v.object({
|
|
23
12
|
id: v.string(),
|
|
@@ -27,6 +16,25 @@ export const vEvent = v.object({
|
|
|
27
16
|
context: v.optional(v.record(v.string(), v.any())),
|
|
28
17
|
});
|
|
29
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Returns the WorkOS user id an event is about, or undefined when the
|
|
21
|
+
* event is about something other than a user.
|
|
22
|
+
*/
|
|
23
|
+
function eventUserId(event: Infer<typeof vEvent>): string | undefined {
|
|
24
|
+
const data = event.data as { id?: unknown; userId?: unknown };
|
|
25
|
+
if (typeof data.userId === "string") {
|
|
26
|
+
return data.userId;
|
|
27
|
+
}
|
|
28
|
+
const isUserEvent =
|
|
29
|
+
event.event === "user.created" ||
|
|
30
|
+
event.event === "user.updated" ||
|
|
31
|
+
event.event === "user.deleted";
|
|
32
|
+
if (isUserEvent && typeof data.id === "string") {
|
|
33
|
+
return data.id;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
async function processEventHandler(
|
|
31
39
|
ctx: MutationCtx,
|
|
32
40
|
args: {
|
|
@@ -38,6 +46,8 @@ async function processEventHandler(
|
|
|
38
46
|
if (args.logLevel === "DEBUG") {
|
|
39
47
|
console.log("processing event", args.event);
|
|
40
48
|
}
|
|
49
|
+
const event = args.event as WorkOSEvent;
|
|
50
|
+
const userId = eventUserId(args.event);
|
|
41
51
|
const dbEvent = await ctx.db
|
|
42
52
|
.query("events")
|
|
43
53
|
.withIndex("eventId", (q) => q.eq("eventId", args.event.id))
|
|
@@ -47,74 +57,94 @@ async function processEventHandler(
|
|
|
47
57
|
return;
|
|
48
58
|
}
|
|
49
59
|
await ctx.db.insert("events", {
|
|
60
|
+
userId,
|
|
50
61
|
eventId: args.event.id,
|
|
51
62
|
event: args.event.event,
|
|
52
63
|
updatedAt: args.event.data.updatedAt as string | undefined,
|
|
53
64
|
});
|
|
54
|
-
|
|
65
|
+
let eventForCallback = event.event;
|
|
66
|
+
let dataForCallback = args.event.data;
|
|
55
67
|
switch (event.event) {
|
|
56
|
-
case "user.created":
|
|
57
|
-
const data = omit(event.data, ["object"]);
|
|
58
|
-
const existingUser = await ctx.db
|
|
59
|
-
.query("users")
|
|
60
|
-
.withIndex("id", (q) => q.eq("id", data.id))
|
|
61
|
-
.unique();
|
|
62
|
-
if (existingUser) {
|
|
63
|
-
console.warn("user already exists", data.id);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
await ctx.db.insert("users", data);
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
68
|
+
case "user.created":
|
|
69
69
|
case "user.updated": {
|
|
70
|
-
const data =
|
|
71
|
-
const
|
|
70
|
+
const data = parse(vUser, event.data);
|
|
71
|
+
const existingUser = await ctx.db
|
|
72
72
|
.query("users")
|
|
73
73
|
.withIndex("id", (q) => q.eq("id", data.id))
|
|
74
74
|
.unique();
|
|
75
|
-
if (!
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
if (!existingUser) {
|
|
76
|
+
const deletedUser = await ctx.db
|
|
77
|
+
.query("deletedUsers")
|
|
78
|
+
.withIndex("id", (q) => q.eq("id", data.id))
|
|
79
|
+
.unique();
|
|
80
|
+
if (deletedUser) {
|
|
81
|
+
console.warn("user already deleted, skipping", event.event, data.id);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
80
84
|
await ctx.db.insert("users", data);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
if (event.event === "user.updated") {
|
|
86
|
+
// WorkOS can deliver the update before the create. The update
|
|
87
|
+
// payload holds the whole user, so it is safe to insert.
|
|
88
|
+
console.warn("user not found for update, inserting", data.id);
|
|
89
|
+
eventForCallback = "user.created";
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
if (event.event === "user.created") {
|
|
93
|
+
console.warn("user already exists", data.id);
|
|
94
|
+
// The callback already fired as user.created when this user was
|
|
95
|
+
// first inserted, so skip it here.
|
|
96
|
+
return;
|
|
97
|
+
} else if (existingUser.updatedAt >= data.updatedAt) {
|
|
98
|
+
console.warn(`user already updated for event ${event.id}, skipping`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
await ctx.db.patch("users", existingUser._id, data);
|
|
86
102
|
}
|
|
87
|
-
await ctx.db.patch("users", user._id, data);
|
|
88
103
|
break;
|
|
89
104
|
}
|
|
90
105
|
case "user.deleted": {
|
|
91
|
-
|
|
106
|
+
// Only the id is needed, so a trimmed payload should not fail the webhook.
|
|
107
|
+
const { id } = parse(v.object({ id: v.string() }), event.data);
|
|
108
|
+
// Record the deletion even if the user was never inserted locally, so a
|
|
109
|
+
// late user.created or user.updated for this user is skipped.
|
|
110
|
+
const deletedUser = await ctx.db
|
|
111
|
+
.query("deletedUsers")
|
|
112
|
+
.withIndex("id", (q) => q.eq("id", id))
|
|
113
|
+
.unique();
|
|
114
|
+
if (!deletedUser) {
|
|
115
|
+
await ctx.db.insert("deletedUsers", { id });
|
|
116
|
+
}
|
|
92
117
|
const user = await ctx.db
|
|
93
118
|
.query("users")
|
|
94
|
-
.withIndex("id", (q) => q.eq("id",
|
|
119
|
+
.withIndex("id", (q) => q.eq("id", id))
|
|
95
120
|
.unique();
|
|
96
121
|
if (!user) {
|
|
97
|
-
console.warn("user not found",
|
|
98
|
-
|
|
122
|
+
console.warn("user not found, forwarding deletion", id);
|
|
123
|
+
break;
|
|
99
124
|
}
|
|
100
125
|
await ctx.db.delete("users", user._id);
|
|
126
|
+
// The callback data is typed as a whole user, so fill anything the
|
|
127
|
+
// payload left out from the stored user.
|
|
128
|
+
dataForCallback = {
|
|
129
|
+
...withoutSystemFields(user),
|
|
130
|
+
object: "user",
|
|
131
|
+
...args.event.data,
|
|
132
|
+
};
|
|
101
133
|
break;
|
|
102
134
|
}
|
|
103
135
|
}
|
|
104
136
|
if (args.onEventHandle) {
|
|
105
137
|
await ctx.runMutation(args.onEventHandle as FunctionHandle<"mutation">, {
|
|
106
|
-
event:
|
|
107
|
-
data:
|
|
138
|
+
event: eventForCallback,
|
|
139
|
+
data: dataForCallback,
|
|
108
140
|
});
|
|
109
141
|
}
|
|
110
142
|
}
|
|
111
143
|
|
|
112
144
|
export const onWebhookEvent = mutation({
|
|
113
145
|
args: {
|
|
114
|
-
apiKey: v.string(),
|
|
115
146
|
event: vEvent,
|
|
116
147
|
onEventHandle: v.optional(v.string()),
|
|
117
|
-
eventTypes: v.optional(v.array(v.string())),
|
|
118
148
|
logLevel: v.optional(v.literal("DEBUG")),
|
|
119
149
|
},
|
|
120
150
|
returns: v.null(),
|
|
@@ -126,84 +156,17 @@ export const onWebhookEvent = mutation({
|
|
|
126
156
|
},
|
|
127
157
|
});
|
|
128
158
|
|
|
129
|
-
export const getCursor = internalQuery({
|
|
130
|
-
args: {},
|
|
131
|
-
returns: v.union(v.string(), v.null()),
|
|
132
|
-
handler: async (ctx) => {
|
|
133
|
-
const lastProcessedEvent = await ctx.db
|
|
134
|
-
.query("events")
|
|
135
|
-
.order("desc")
|
|
136
|
-
.first();
|
|
137
|
-
return lastProcessedEvent?.eventId;
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
export const updateEvents = internalAction({
|
|
142
|
-
args: {
|
|
143
|
-
apiKey: v.string(),
|
|
144
|
-
onEventHandle: v.optional(v.string()),
|
|
145
|
-
eventTypes: v.optional(v.array(v.string())),
|
|
146
|
-
logLevel: v.optional(v.literal("DEBUG")),
|
|
147
|
-
},
|
|
148
|
-
handler: async (ctx, args) => {
|
|
149
|
-
// Cancel other pending workpool jobs since this run will
|
|
150
|
-
// process all available events from the WorkOS API.
|
|
151
|
-
await eventWorkpool.cancelAll(ctx);
|
|
152
|
-
const workos = new WorkOS(args.apiKey);
|
|
153
|
-
const cursor = await ctx.runQuery(internal.lib.getCursor);
|
|
154
|
-
let nextCursor = cursor ?? undefined;
|
|
155
|
-
const eventTypes = [
|
|
156
|
-
"user.created" as const,
|
|
157
|
-
"user.updated" as const,
|
|
158
|
-
"user.deleted" as const,
|
|
159
|
-
...((args.eventTypes as WorkOSEvent["event"][]) ?? []),
|
|
160
|
-
];
|
|
161
|
-
// No cursor should mean we haven't handled any events - set
|
|
162
|
-
// a start time of 5 minutes ago
|
|
163
|
-
let rangeStart = nextCursor
|
|
164
|
-
? undefined
|
|
165
|
-
: new Date(Date.now() - 1000 * 60 * 5).toISOString();
|
|
166
|
-
do {
|
|
167
|
-
const { data, listMetadata } = await workos.events.listEvents({
|
|
168
|
-
events: eventTypes,
|
|
169
|
-
after: nextCursor,
|
|
170
|
-
rangeStart,
|
|
171
|
-
});
|
|
172
|
-
for (const event of data) {
|
|
173
|
-
await ctx.runMutation(internal.lib.processEvent, {
|
|
174
|
-
event: parse(vEvent, event),
|
|
175
|
-
logLevel: args.logLevel,
|
|
176
|
-
onEventHandle: args.onEventHandle,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
nextCursor = listMetadata.after ?? undefined;
|
|
180
|
-
rangeStart = undefined;
|
|
181
|
-
} while (nextCursor);
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
export const processEvent = internalMutation({
|
|
186
|
-
args: {
|
|
187
|
-
event: vEvent,
|
|
188
|
-
logLevel: v.optional(v.literal("DEBUG")),
|
|
189
|
-
onEventHandle: v.optional(v.string()),
|
|
190
|
-
},
|
|
191
|
-
handler: async (ctx, args) => {
|
|
192
|
-
await processEventHandler(ctx, args);
|
|
193
|
-
},
|
|
194
|
-
});
|
|
195
|
-
|
|
196
159
|
export const getAuthUser = query({
|
|
197
160
|
args: {
|
|
198
161
|
id: v.string(),
|
|
199
162
|
},
|
|
200
|
-
returns: v.union(
|
|
163
|
+
returns: v.union(vUser, v.null()),
|
|
201
164
|
handler: async (ctx, args) => {
|
|
202
165
|
const user = await ctx.db
|
|
203
166
|
.query("users")
|
|
204
167
|
.withIndex("id", (q) => q.eq("id", args.id))
|
|
205
168
|
.unique();
|
|
206
|
-
return
|
|
169
|
+
return publicUser(user);
|
|
207
170
|
},
|
|
208
171
|
});
|
|
209
172
|
|
|
@@ -211,12 +174,20 @@ export const getAuthUserByExternalId = query({
|
|
|
211
174
|
args: {
|
|
212
175
|
externalId: v.string(),
|
|
213
176
|
},
|
|
214
|
-
returns: v.union(
|
|
177
|
+
returns: v.union(vUser, v.null()),
|
|
215
178
|
handler: async (ctx, args) => {
|
|
216
179
|
const user = await ctx.db
|
|
217
180
|
.query("users")
|
|
218
181
|
.withIndex("externalId", (q) => q.eq("externalId", args.externalId))
|
|
219
182
|
.unique();
|
|
220
|
-
return
|
|
183
|
+
return publicUser(user);
|
|
221
184
|
},
|
|
222
185
|
});
|
|
186
|
+
|
|
187
|
+
/** Drops the Convex system fields so the user matches the public vUser shape. */
|
|
188
|
+
function publicUser(user: Doc<"users"> | null): Infer<typeof vUser> | null {
|
|
189
|
+
if (!user) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
return withoutSystemFields(user);
|
|
193
|
+
}
|
package/src/component/schema.ts
CHANGED
|
@@ -1,28 +1,21 @@
|
|
|
1
1
|
import { defineSchema, defineTable } from "convex/server";
|
|
2
2
|
import { v } from "convex/values";
|
|
3
|
+
import { vUser } from "../validators.js";
|
|
3
4
|
|
|
4
5
|
export default defineSchema({
|
|
5
6
|
events: defineTable({
|
|
6
7
|
eventId: v.string(),
|
|
7
8
|
event: v.string(),
|
|
8
9
|
updatedAt: v.optional(v.string()),
|
|
10
|
+
userId: v.optional(v.string()),
|
|
9
11
|
}).index("eventId", ["eventId"]),
|
|
10
12
|
backfillState: defineTable({
|
|
11
13
|
apiKey: v.string(),
|
|
12
14
|
}),
|
|
13
|
-
users: defineTable(
|
|
15
|
+
users: defineTable(vUser)
|
|
16
|
+
.index("id", ["id"])
|
|
17
|
+
.index("externalId", ["externalId"]),
|
|
18
|
+
deletedUsers: defineTable({
|
|
14
19
|
id: v.string(),
|
|
15
|
-
|
|
16
|
-
name: v.optional(v.union(v.null(), v.string())),
|
|
17
|
-
firstName: v.optional(v.union(v.null(), v.string())),
|
|
18
|
-
lastName: v.optional(v.union(v.null(), v.string())),
|
|
19
|
-
emailVerified: v.boolean(),
|
|
20
|
-
profilePictureUrl: v.optional(v.union(v.null(), v.string())),
|
|
21
|
-
lastSignInAt: v.optional(v.union(v.null(), v.string())),
|
|
22
|
-
externalId: v.optional(v.union(v.null(), v.string())),
|
|
23
|
-
metadata: v.record(v.string(), v.any()),
|
|
24
|
-
locale: v.optional(v.union(v.null(), v.string())),
|
|
25
|
-
createdAt: v.string(),
|
|
26
|
-
updatedAt: v.string(),
|
|
27
|
-
}).index("id", ["id"]).index("externalId", ["externalId"]),
|
|
20
|
+
}).index("id", ["id"]),
|
|
28
21
|
});
|
package/src/test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TestConvex } from "convex-test";
|
|
2
2
|
import type { GenericSchema, SchemaDefinition } from "convex/server";
|
|
3
|
-
import workpool from "@convex-dev/workpool/test";
|
|
4
3
|
import workflow from "@convex-dev/workflow/test";
|
|
4
|
+
import type { ComponentApi } from "./component/_generated/component.js";
|
|
5
5
|
import schema from "./component/schema.js";
|
|
6
6
|
const modules = import.meta.glob("./component/**/*.ts");
|
|
7
7
|
|
|
@@ -9,13 +9,14 @@ const modules = import.meta.glob("./component/**/*.ts");
|
|
|
9
9
|
* Register the component with the test convex instance.
|
|
10
10
|
* @param t - The test convex instance, e.g. from calling `convexTest`.
|
|
11
11
|
* @param name - The name of the component, as registered in convex.config.ts.
|
|
12
|
+
* @returns a component api to test via ctx.runMutation or for thick client
|
|
13
|
+
* usage. Also provides types for convex-test's defineTestApp.
|
|
12
14
|
*/
|
|
13
15
|
function register(
|
|
14
16
|
t: TestConvex<SchemaDefinition<GenericSchema, boolean>>,
|
|
15
17
|
name: string = "workOSAuthKit"
|
|
16
18
|
) {
|
|
17
|
-
t.registerComponent(name, schema, modules);
|
|
18
|
-
workpool.register(t, `${name}/eventWorkpool`);
|
|
19
19
|
workflow.register(t, `${name}/backfillWorkflow`);
|
|
20
|
+
return t.registerComponent(name, schema, modules) as unknown as ComponentApi;
|
|
20
21
|
}
|
|
21
22
|
export default { register, schema, modules };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
|
|
3
|
+
export const vUser = v.object({
|
|
4
|
+
id: v.string(),
|
|
5
|
+
email: v.string(),
|
|
6
|
+
name: v.optional(v.union(v.null(), v.string())),
|
|
7
|
+
firstName: v.optional(v.union(v.null(), v.string())),
|
|
8
|
+
lastName: v.optional(v.union(v.null(), v.string())),
|
|
9
|
+
emailVerified: v.boolean(),
|
|
10
|
+
profilePictureUrl: v.optional(v.union(v.null(), v.string())),
|
|
11
|
+
lastSignInAt: v.optional(v.union(v.null(), v.string())),
|
|
12
|
+
externalId: v.optional(v.union(v.null(), v.string())),
|
|
13
|
+
metadata: v.record(v.string(), v.any()),
|
|
14
|
+
locale: v.optional(v.union(v.null(), v.string())),
|
|
15
|
+
createdAt: v.string(),
|
|
16
|
+
updatedAt: v.string(),
|
|
17
|
+
});
|