@convex-dev/workos-authkit 0.2.8 → 0.2.10
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 +4 -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/_generated/server.d.ts +12 -0
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js +2 -0
- package/dist/component/_generated/server.js.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 +94 -122
- 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 +5 -5
- 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/_generated/server.ts +13 -0
- 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 +370 -0
- package/src/component/lib.ts +97 -133
- package/src/component/schema.ts +7 -14
- package/src/test.ts +0 -2
- 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,156 +57,102 @@ 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
|
-
|
|
68
|
+
case "user.created":
|
|
69
|
+
case "user.updated": {
|
|
70
|
+
const data = parse(vUser, event.data);
|
|
58
71
|
const existingUser = await ctx.db
|
|
59
72
|
.query("users")
|
|
60
73
|
.withIndex("id", (q) => q.eq("id", data.id))
|
|
61
74
|
.unique();
|
|
62
|
-
if (existingUser) {
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
}
|
|
84
|
+
await ctx.db.insert("users", data);
|
|
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);
|
|
65
102
|
}
|
|
66
|
-
await ctx.db.insert("users", data);
|
|
67
103
|
break;
|
|
68
104
|
}
|
|
69
|
-
case "user.
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
105
|
+
case "user.deleted": {
|
|
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))
|
|
74
113
|
.unique();
|
|
75
|
-
if (!
|
|
76
|
-
|
|
77
|
-
return;
|
|
114
|
+
if (!deletedUser) {
|
|
115
|
+
await ctx.db.insert("deletedUsers", { id });
|
|
78
116
|
}
|
|
79
|
-
if (user.updatedAt >= data.updatedAt) {
|
|
80
|
-
console.warn(`user already updated for event ${event.id}, skipping`);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
await ctx.db.patch("users", user._id, data);
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
case "user.deleted": {
|
|
87
|
-
const data = omit(event.data, ["object"]);
|
|
88
117
|
const user = await ctx.db
|
|
89
118
|
.query("users")
|
|
90
|
-
.withIndex("id", (q) => q.eq("id",
|
|
119
|
+
.withIndex("id", (q) => q.eq("id", id))
|
|
91
120
|
.unique();
|
|
92
121
|
if (!user) {
|
|
93
|
-
console.warn("user not found",
|
|
122
|
+
console.warn("user not found, skipping deletion", id);
|
|
94
123
|
return;
|
|
95
124
|
}
|
|
96
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
|
+
};
|
|
97
133
|
break;
|
|
98
134
|
}
|
|
99
135
|
}
|
|
100
136
|
if (args.onEventHandle) {
|
|
101
137
|
await ctx.runMutation(args.onEventHandle as FunctionHandle<"mutation">, {
|
|
102
|
-
event:
|
|
103
|
-
data:
|
|
138
|
+
event: eventForCallback,
|
|
139
|
+
data: dataForCallback,
|
|
104
140
|
});
|
|
105
141
|
}
|
|
106
142
|
}
|
|
107
143
|
|
|
108
144
|
export const onWebhookEvent = mutation({
|
|
109
145
|
args: {
|
|
110
|
-
apiKey: v.string(),
|
|
111
146
|
event: vEvent,
|
|
112
147
|
onEventHandle: v.optional(v.string()),
|
|
113
|
-
eventTypes: v.optional(v.array(v.string())),
|
|
114
148
|
logLevel: v.optional(v.literal("DEBUG")),
|
|
115
149
|
},
|
|
116
150
|
returns: v.null(),
|
|
117
151
|
handler: async (ctx, args) => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (isCreateEvent) {
|
|
121
|
-
// Process create events immediately
|
|
122
|
-
await processEventHandler(ctx, args);
|
|
123
|
-
} else {
|
|
124
|
-
// Enqueue update/delete events to workpool
|
|
125
|
-
await eventWorkpool.enqueueAction(ctx, internal.lib.updateEvents, {
|
|
126
|
-
apiKey: args.apiKey,
|
|
127
|
-
onEventHandle: args.onEventHandle,
|
|
128
|
-
eventTypes: args.eventTypes,
|
|
129
|
-
logLevel: args.logLevel,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
return null;
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
export const getCursor = internalQuery({
|
|
137
|
-
args: {},
|
|
138
|
-
returns: v.union(v.string(), v.null()),
|
|
139
|
-
handler: async (ctx) => {
|
|
140
|
-
const lastProcessedEvent = await ctx.db
|
|
141
|
-
.query("events")
|
|
142
|
-
.order("desc")
|
|
143
|
-
.first();
|
|
144
|
-
return lastProcessedEvent?.eventId;
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
export const updateEvents = internalAction({
|
|
149
|
-
args: {
|
|
150
|
-
apiKey: v.string(),
|
|
151
|
-
onEventHandle: v.optional(v.string()),
|
|
152
|
-
eventTypes: v.optional(v.array(v.string())),
|
|
153
|
-
logLevel: v.optional(v.literal("DEBUG")),
|
|
154
|
-
},
|
|
155
|
-
handler: async (ctx, args) => {
|
|
156
|
-
// Cancel other pending workpool jobs since this run will
|
|
157
|
-
// process all available events from the WorkOS API.
|
|
158
|
-
await eventWorkpool.cancelAll(ctx);
|
|
159
|
-
const workos = new WorkOS(args.apiKey);
|
|
160
|
-
const cursor = await ctx.runQuery(internal.lib.getCursor);
|
|
161
|
-
let nextCursor = cursor ?? undefined;
|
|
162
|
-
const eventTypes = [
|
|
163
|
-
"user.created" as const,
|
|
164
|
-
"user.updated" as const,
|
|
165
|
-
"user.deleted" as const,
|
|
166
|
-
...((args.eventTypes as WorkOSEvent["event"][]) ?? []),
|
|
167
|
-
];
|
|
168
|
-
// No cursor should mean we haven't handled any events - set
|
|
169
|
-
// a start time of 5 minutes ago
|
|
170
|
-
let rangeStart = nextCursor
|
|
171
|
-
? undefined
|
|
172
|
-
: new Date(Date.now() - 1000 * 60 * 5).toISOString();
|
|
173
|
-
do {
|
|
174
|
-
const { data, listMetadata } = await workos.events.listEvents({
|
|
175
|
-
events: eventTypes,
|
|
176
|
-
after: nextCursor,
|
|
177
|
-
rangeStart,
|
|
178
|
-
});
|
|
179
|
-
for (const event of data) {
|
|
180
|
-
await ctx.runMutation(internal.lib.processEvent, {
|
|
181
|
-
event: parse(vEvent, event),
|
|
182
|
-
logLevel: args.logLevel,
|
|
183
|
-
onEventHandle: args.onEventHandle,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
nextCursor = listMetadata.after ?? undefined;
|
|
187
|
-
rangeStart = undefined;
|
|
188
|
-
} while (nextCursor);
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
export const processEvent = internalMutation({
|
|
193
|
-
args: {
|
|
194
|
-
event: vEvent,
|
|
195
|
-
logLevel: v.optional(v.literal("DEBUG")),
|
|
196
|
-
onEventHandle: v.optional(v.string()),
|
|
197
|
-
},
|
|
198
|
-
handler: async (ctx, args) => {
|
|
152
|
+
// The payload is signature-verified and dedupes on eventId, so all
|
|
153
|
+
// event types can be processed inline.
|
|
199
154
|
await processEventHandler(ctx, args);
|
|
155
|
+
return null;
|
|
200
156
|
},
|
|
201
157
|
});
|
|
202
158
|
|
|
@@ -204,13 +160,13 @@ export const getAuthUser = query({
|
|
|
204
160
|
args: {
|
|
205
161
|
id: v.string(),
|
|
206
162
|
},
|
|
207
|
-
returns: v.union(
|
|
163
|
+
returns: v.union(vUser, v.null()),
|
|
208
164
|
handler: async (ctx, args) => {
|
|
209
165
|
const user = await ctx.db
|
|
210
166
|
.query("users")
|
|
211
167
|
.withIndex("id", (q) => q.eq("id", args.id))
|
|
212
168
|
.unique();
|
|
213
|
-
return
|
|
169
|
+
return publicUser(user);
|
|
214
170
|
},
|
|
215
171
|
});
|
|
216
172
|
|
|
@@ -218,12 +174,20 @@ export const getAuthUserByExternalId = query({
|
|
|
218
174
|
args: {
|
|
219
175
|
externalId: v.string(),
|
|
220
176
|
},
|
|
221
|
-
returns: v.union(
|
|
177
|
+
returns: v.union(vUser, v.null()),
|
|
222
178
|
handler: async (ctx, args) => {
|
|
223
179
|
const user = await ctx.db
|
|
224
180
|
.query("users")
|
|
225
181
|
.withIndex("externalId", (q) => q.eq("externalId", args.externalId))
|
|
226
182
|
.unique();
|
|
227
|
-
return
|
|
183
|
+
return publicUser(user);
|
|
228
184
|
},
|
|
229
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,6 +1,5 @@
|
|
|
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";
|
|
5
4
|
import schema from "./component/schema.js";
|
|
6
5
|
const modules = import.meta.glob("./component/**/*.ts");
|
|
@@ -15,7 +14,6 @@ function register(
|
|
|
15
14
|
name: string = "workOSAuthKit"
|
|
16
15
|
) {
|
|
17
16
|
t.registerComponent(name, schema, modules);
|
|
18
|
-
workpool.register(t, `${name}/eventWorkpool`);
|
|
19
17
|
workflow.register(t, `${name}/backfillWorkflow`);
|
|
20
18
|
}
|
|
21
19
|
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
|
+
});
|