@rawdash/connector-clerk 0.24.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 +134 -0
- package/dist/index.d.ts +476 -0
- package/dist/index.js +638 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
// ../../connector-shared/dist/index.js
|
|
2
|
+
var HTTP_CLIENT_VERSION = "0.0.0";
|
|
3
|
+
var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
|
|
4
|
+
function connectorUserAgent(connectorId) {
|
|
5
|
+
return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
|
|
6
|
+
}
|
|
7
|
+
function standardRateLimitPolicy(config) {
|
|
8
|
+
const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;
|
|
9
|
+
const multiplier = resetUnit === "s" ? 1e3 : 1;
|
|
10
|
+
return {
|
|
11
|
+
parse(h) {
|
|
12
|
+
const remainingRaw = h.get(remainingHeader);
|
|
13
|
+
if (remainingRaw === null || remainingRaw.trim() === "") {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const remaining = Number(remainingRaw);
|
|
17
|
+
if (!Number.isFinite(remaining)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const resetRaw = h.get(resetHeader);
|
|
21
|
+
if (resetRaw === null) {
|
|
22
|
+
if (resetFallbackMs === void 0) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
remaining,
|
|
27
|
+
resetAt: new Date(Date.now() + resetFallbackMs)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (resetRaw.trim() === "") {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const reset = Number(resetRaw);
|
|
34
|
+
if (!Number.isFinite(reset) || reset < 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const resetMs = reset * multiplier;
|
|
38
|
+
if (!Number.isFinite(resetMs)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return { remaining, resetAt: new Date(resetMs) };
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function parseEpoch(value, unit) {
|
|
46
|
+
if (value === null || value === void 0) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (unit === "iso") {
|
|
50
|
+
if (typeof value !== "string") {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const ms = new Date(value).getTime();
|
|
54
|
+
return Number.isFinite(ms) ? ms : null;
|
|
55
|
+
}
|
|
56
|
+
if (typeof value === "string" && value.trim() === "") {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
60
|
+
if (!Number.isFinite(n)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const result = unit === "s" ? n * 1e3 : n;
|
|
64
|
+
return Number.isFinite(result) ? result : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/clerk.ts
|
|
68
|
+
import {
|
|
69
|
+
BaseConnector,
|
|
70
|
+
defineConfigFields,
|
|
71
|
+
defineConnectorDoc,
|
|
72
|
+
defineResources,
|
|
73
|
+
makeChunkedCursorGuard,
|
|
74
|
+
paginateChunked,
|
|
75
|
+
schemasFromResources,
|
|
76
|
+
selectActivePhases
|
|
77
|
+
} from "@rawdash/core";
|
|
78
|
+
import { z } from "zod";
|
|
79
|
+
var configFields = defineConfigFields(
|
|
80
|
+
z.object({
|
|
81
|
+
secretKey: z.object({ $secret: z.string().min(1) }).meta({
|
|
82
|
+
label: "Secret key",
|
|
83
|
+
description: "Clerk Backend API secret key (starts with `sk_test_` or `sk_live_`). Create one at Clerk Dashboard -> API Keys.",
|
|
84
|
+
placeholder: "CLERK_SECRET_KEY",
|
|
85
|
+
secret: true
|
|
86
|
+
}),
|
|
87
|
+
apiUrl: z.string().trim().url('Must be a full URL, e.g. "https://api.clerk.com".').default("https://api.clerk.com").meta({
|
|
88
|
+
label: "API base URL",
|
|
89
|
+
description: "Clerk Backend API base URL. Defaults to https://api.clerk.com; override only if you are pinned to the legacy https://api.clerk.dev host.",
|
|
90
|
+
placeholder: "https://api.clerk.com"
|
|
91
|
+
}),
|
|
92
|
+
resources: z.array(
|
|
93
|
+
z.enum(["users", "organizations", "sessions", "daily_active_users"])
|
|
94
|
+
).nonempty().optional().meta({
|
|
95
|
+
label: "Resources",
|
|
96
|
+
description: "Which Clerk resources to sync. Omit to sync all of them. The secret key has read access to every resource by default; the allowlist exists to skip phases your dashboards do not query."
|
|
97
|
+
}),
|
|
98
|
+
dauLookbackDays: z.number().int().positive().max(90).optional().meta({
|
|
99
|
+
label: "DAU lookback (days)",
|
|
100
|
+
description: "How many days back to bucket users by last_active_at when computing the daily_active_users metric. Defaults to 30; the cap is 90.",
|
|
101
|
+
placeholder: "30"
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
var doc = defineConnectorDoc({
|
|
106
|
+
displayName: "Clerk",
|
|
107
|
+
category: "security",
|
|
108
|
+
brandColor: "#6C47FF",
|
|
109
|
+
tagline: "Sync users, organizations, sessions, and a derived daily-active-users metric from a Clerk application for sign-up, DAU, and active-session dashboards.",
|
|
110
|
+
vendor: {
|
|
111
|
+
name: "Clerk",
|
|
112
|
+
domain: "clerk.com",
|
|
113
|
+
apiDocs: "https://clerk.com/docs/reference/backend-api",
|
|
114
|
+
website: "https://clerk.com"
|
|
115
|
+
},
|
|
116
|
+
auth: {
|
|
117
|
+
summary: "A Clerk Backend API secret key (Bearer token). Anyone with the key has read access to every resource the connector syncs.",
|
|
118
|
+
setup: [
|
|
119
|
+
"Open the Clerk Dashboard for the application you want to sync and navigate to API Keys.",
|
|
120
|
+
"Copy the Secret key (it starts with `sk_test_` for development instances or `sk_live_` for production).",
|
|
121
|
+
'Store it as a rawdash secret and reference it from the connector config as `secretKey: secret("CLERK_SECRET_KEY")`.',
|
|
122
|
+
"Treat the secret key like a root credential - rotate it from the dashboard if it leaks."
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
rateLimit: "Clerk Backend API throttles per instance (~20 req/s for production, lower for dev). Responses publish X-RateLimit-Remaining / X-RateLimit-Reset (Unix seconds) headers and the shared HTTP client backs off on 429 using the standard rate-limit policy.",
|
|
126
|
+
limitations: [
|
|
127
|
+
"Each phase paginates via limit / offset and is capped at 50 pages per sync (~25,000 rows). Instances larger than that should run more frequent incremental syncs so each window fits under the cap.",
|
|
128
|
+
"The daily_active_users metric is derived by bucketing users by the day of their last_active_at timestamp - it counts users whose most recent activity fell on each day, not unique users active across overlapping days.",
|
|
129
|
+
"Webhooks, JWT templates, instance settings, and impersonation tokens are out of scope."
|
|
130
|
+
]
|
|
131
|
+
});
|
|
132
|
+
var clerkCredentials = {
|
|
133
|
+
secretKey: {
|
|
134
|
+
description: "Clerk Backend API secret key",
|
|
135
|
+
auth: "required"
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
var clerkRateLimit = standardRateLimitPolicy({
|
|
139
|
+
remainingHeader: "x-ratelimit-remaining",
|
|
140
|
+
resetHeader: "x-ratelimit-reset",
|
|
141
|
+
resetUnit: "s"
|
|
142
|
+
});
|
|
143
|
+
var PHASE_ORDER = [
|
|
144
|
+
"users",
|
|
145
|
+
"organizations",
|
|
146
|
+
"sessions",
|
|
147
|
+
"daily_active_users"
|
|
148
|
+
];
|
|
149
|
+
var isClerkSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);
|
|
150
|
+
var USER_ENTITY = "clerk_user";
|
|
151
|
+
var ORG_ENTITY = "clerk_organization";
|
|
152
|
+
var SESSION_EVENT = "clerk_session";
|
|
153
|
+
var DAU_METRIC = "clerk_daily_active_users";
|
|
154
|
+
var PAGE_SIZE = 500;
|
|
155
|
+
var MAX_PAGES = 50;
|
|
156
|
+
var DEFAULT_DAU_LOOKBACK_DAYS = 30;
|
|
157
|
+
var DEFAULT_API_URL = "https://api.clerk.com";
|
|
158
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
159
|
+
var SESSION_STATUSES = [
|
|
160
|
+
"abandoned",
|
|
161
|
+
"active",
|
|
162
|
+
"ended",
|
|
163
|
+
"expired",
|
|
164
|
+
"removed",
|
|
165
|
+
"replaced",
|
|
166
|
+
"revoked"
|
|
167
|
+
];
|
|
168
|
+
var idString = z.string().min(1);
|
|
169
|
+
var emailAddressSchema = z.object({
|
|
170
|
+
id: z.string().optional(),
|
|
171
|
+
email_address: z.string().nullish(),
|
|
172
|
+
verification: z.object({ status: z.string().nullish() }).nullish()
|
|
173
|
+
});
|
|
174
|
+
var userSchema = z.object({
|
|
175
|
+
id: idString,
|
|
176
|
+
primary_email_address_id: z.string().nullish(),
|
|
177
|
+
email_addresses: z.array(emailAddressSchema).nullish(),
|
|
178
|
+
first_name: z.string().nullish(),
|
|
179
|
+
last_name: z.string().nullish(),
|
|
180
|
+
username: z.string().nullish(),
|
|
181
|
+
last_sign_in_at: z.number().nullish(),
|
|
182
|
+
last_active_at: z.number().nullish(),
|
|
183
|
+
created_at: z.number().nullish(),
|
|
184
|
+
updated_at: z.number().nullish(),
|
|
185
|
+
banned: z.boolean().nullish(),
|
|
186
|
+
locked: z.boolean().nullish()
|
|
187
|
+
});
|
|
188
|
+
var usersResponseSchema = z.array(userSchema);
|
|
189
|
+
var organizationSchema = z.object({
|
|
190
|
+
id: idString,
|
|
191
|
+
name: z.string().nullish(),
|
|
192
|
+
slug: z.string().nullish(),
|
|
193
|
+
members_count: z.number().nullish(),
|
|
194
|
+
created_at: z.number().nullish(),
|
|
195
|
+
updated_at: z.number().nullish()
|
|
196
|
+
});
|
|
197
|
+
var organizationsResponseSchema = z.union([
|
|
198
|
+
z.object({
|
|
199
|
+
data: z.array(organizationSchema),
|
|
200
|
+
total_count: z.number().optional()
|
|
201
|
+
}),
|
|
202
|
+
z.array(organizationSchema)
|
|
203
|
+
]);
|
|
204
|
+
var sessionSchema = z.object({
|
|
205
|
+
id: idString,
|
|
206
|
+
user_id: z.string().nullish(),
|
|
207
|
+
client_id: z.string().nullish(),
|
|
208
|
+
status: z.string(),
|
|
209
|
+
last_active_at: z.number().nullish(),
|
|
210
|
+
expire_at: z.number().nullish(),
|
|
211
|
+
abandon_at: z.number().nullish(),
|
|
212
|
+
created_at: z.number().nullish(),
|
|
213
|
+
updated_at: z.number().nullish()
|
|
214
|
+
});
|
|
215
|
+
var sessionsResponseSchema = z.array(sessionSchema);
|
|
216
|
+
var clerkResources = defineResources({
|
|
217
|
+
[USER_ENTITY]: {
|
|
218
|
+
shape: "entity",
|
|
219
|
+
filterable: [
|
|
220
|
+
{ field: "banned", ops: ["eq"], values: ["true", "false"] },
|
|
221
|
+
{ field: "locked", ops: ["eq"], values: ["true", "false"] }
|
|
222
|
+
],
|
|
223
|
+
description: "Clerk users keyed by user id, with primary email, sign-in / activity timestamps, and banned / locked flags.",
|
|
224
|
+
endpoint: "GET /v1/users",
|
|
225
|
+
notes: "Uses offset pagination (limit / offset) capped at 50 pages (~25,000 users) per sync. Incremental syncs pass options.since through as the last_active_at_since filter.",
|
|
226
|
+
fields: [
|
|
227
|
+
{ name: "email", description: "Primary email address (when present)." },
|
|
228
|
+
{
|
|
229
|
+
name: "emailVerified",
|
|
230
|
+
description: "Whether the primary email address is verified (null if no email is set)."
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: "lastSignInAt",
|
|
234
|
+
description: "Most recent sign-in timestamp (Unix ms)."
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "lastActiveAt",
|
|
238
|
+
description: "Most recent activity timestamp (Unix ms). Clerk updates this on every successful client request."
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: "banned",
|
|
242
|
+
description: "Whether the user has been banned."
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: "locked",
|
|
246
|
+
description: "Whether the user is locked from signing in."
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "createdAt",
|
|
250
|
+
description: "When the user account was created (Unix ms)."
|
|
251
|
+
}
|
|
252
|
+
],
|
|
253
|
+
responses: { users: usersResponseSchema }
|
|
254
|
+
},
|
|
255
|
+
[ORG_ENTITY]: {
|
|
256
|
+
shape: "entity",
|
|
257
|
+
filterable: [],
|
|
258
|
+
description: "Clerk organizations keyed by organization id, with display name, slug, and members count.",
|
|
259
|
+
endpoint: "GET /v1/organizations",
|
|
260
|
+
notes: "Uses offset pagination (limit / offset) capped at 50 pages. Clerk has no created_at / updated_at filter for organizations, so each sync re-scans the full list and short-circuits once a page is entirely older than options.since.",
|
|
261
|
+
fields: [
|
|
262
|
+
{ name: "name", description: "Organization display name." },
|
|
263
|
+
{ name: "slug", description: "Organization URL slug." },
|
|
264
|
+
{
|
|
265
|
+
name: "membersCount",
|
|
266
|
+
description: "Number of users in the organization at sync time."
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: "createdAt",
|
|
270
|
+
description: "When the organization was created (Unix ms)."
|
|
271
|
+
}
|
|
272
|
+
],
|
|
273
|
+
responses: { organizations: organizationsResponseSchema }
|
|
274
|
+
},
|
|
275
|
+
[SESSION_EVENT]: {
|
|
276
|
+
shape: "event",
|
|
277
|
+
filterable: [
|
|
278
|
+
{
|
|
279
|
+
field: "status",
|
|
280
|
+
ops: ["eq"],
|
|
281
|
+
values: SESSION_STATUSES
|
|
282
|
+
}
|
|
283
|
+
],
|
|
284
|
+
description: "Clerk session events. One event per session row with start_ts set to created_at and attributes carrying user id, status, and last activity.",
|
|
285
|
+
endpoint: "GET /v1/sessions",
|
|
286
|
+
notes: "Uses offset pagination (limit / offset) capped at 50 pages. Clerk has no since filter on /v1/sessions, so the sync walks newest-first and stops once a page is entirely older than options.since.",
|
|
287
|
+
fields: [
|
|
288
|
+
{ name: "sessionId", description: "Clerk session id." },
|
|
289
|
+
{ name: "userId", description: "User the session belongs to." },
|
|
290
|
+
{
|
|
291
|
+
name: "status",
|
|
292
|
+
description: "Session status (active | ended | expired | abandoned | removed | replaced | revoked)."
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: "lastActiveAt",
|
|
296
|
+
description: "Most recent activity timestamp on the session (Unix ms)."
|
|
297
|
+
}
|
|
298
|
+
],
|
|
299
|
+
responses: { sessions: sessionsResponseSchema }
|
|
300
|
+
},
|
|
301
|
+
[DAU_METRIC]: {
|
|
302
|
+
shape: "metric",
|
|
303
|
+
description: "Daily active users derived from the Clerk users endpoint: one sample per UTC day in the configured lookback window, counting users whose last_active_at fell on that day.",
|
|
304
|
+
endpoint: "GET /v1/users",
|
|
305
|
+
unit: "count",
|
|
306
|
+
granularity: "1d",
|
|
307
|
+
dimensions: [],
|
|
308
|
+
responses: { dau_users: usersResponseSchema }
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
var id = "clerk";
|
|
312
|
+
function primaryEmail(user) {
|
|
313
|
+
const list = user.email_addresses ?? [];
|
|
314
|
+
if (list.length === 0) {
|
|
315
|
+
return { email: null, verified: null };
|
|
316
|
+
}
|
|
317
|
+
const primaryId = user.primary_email_address_id ?? null;
|
|
318
|
+
const primary = (primaryId !== null ? list.find((e) => e.id === primaryId) : void 0) ?? list[0];
|
|
319
|
+
const verified = primary.verification?.status === "verified" ? true : primary.verification?.status ? false : null;
|
|
320
|
+
return { email: primary.email_address ?? null, verified };
|
|
321
|
+
}
|
|
322
|
+
function isSessionStatus(value) {
|
|
323
|
+
return SESSION_STATUSES.includes(value);
|
|
324
|
+
}
|
|
325
|
+
function dayBucket(tsMs) {
|
|
326
|
+
return Math.floor(tsMs / DAY_MS) * DAY_MS;
|
|
327
|
+
}
|
|
328
|
+
function unwrapOrganizations(body) {
|
|
329
|
+
if (Array.isArray(body)) {
|
|
330
|
+
return { items: body, totalCount: null };
|
|
331
|
+
}
|
|
332
|
+
return { items: body.data, totalCount: body.total_count ?? null };
|
|
333
|
+
}
|
|
334
|
+
var ClerkConnector = class _ClerkConnector extends BaseConnector {
|
|
335
|
+
static id = id;
|
|
336
|
+
static resources = clerkResources;
|
|
337
|
+
static schemas = schemasFromResources(clerkResources);
|
|
338
|
+
static create(input, ctx) {
|
|
339
|
+
const parsed = configFields.parse(input);
|
|
340
|
+
return new _ClerkConnector(
|
|
341
|
+
{
|
|
342
|
+
apiUrl: parsed.apiUrl,
|
|
343
|
+
resources: parsed.resources,
|
|
344
|
+
dauLookbackDays: parsed.dauLookbackDays
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
secretKey: parsed.secretKey
|
|
348
|
+
},
|
|
349
|
+
ctx
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
id = id;
|
|
353
|
+
credentials = clerkCredentials;
|
|
354
|
+
dauBuckets = /* @__PURE__ */ new Map();
|
|
355
|
+
baseUrl() {
|
|
356
|
+
const raw = this.settings.apiUrl ?? DEFAULT_API_URL;
|
|
357
|
+
return raw.replace(/\/+$/, "");
|
|
358
|
+
}
|
|
359
|
+
dauLookbackDays() {
|
|
360
|
+
return this.settings.dauLookbackDays ?? DEFAULT_DAU_LOOKBACK_DAYS;
|
|
361
|
+
}
|
|
362
|
+
dauCutoffMs() {
|
|
363
|
+
return Date.now() - this.dauLookbackDays() * DAY_MS;
|
|
364
|
+
}
|
|
365
|
+
parsePageCursor(page) {
|
|
366
|
+
if (!page) {
|
|
367
|
+
return 0;
|
|
368
|
+
}
|
|
369
|
+
const n = Number.parseInt(page, 10);
|
|
370
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
371
|
+
return 0;
|
|
372
|
+
}
|
|
373
|
+
return n;
|
|
374
|
+
}
|
|
375
|
+
async apiGet(url, resource, signal) {
|
|
376
|
+
return this.get(url, {
|
|
377
|
+
resource,
|
|
378
|
+
headers: {
|
|
379
|
+
Authorization: `Bearer ${this.creds.secretKey}`,
|
|
380
|
+
Accept: "application/json",
|
|
381
|
+
"User-Agent": connectorUserAgent("clerk")
|
|
382
|
+
},
|
|
383
|
+
rateLimit: clerkRateLimit,
|
|
384
|
+
signal
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
buildUsersUrl(offset, options) {
|
|
388
|
+
const u = new URL(`${this.baseUrl()}/v1/users`);
|
|
389
|
+
u.searchParams.set("limit", String(PAGE_SIZE));
|
|
390
|
+
u.searchParams.set("offset", String(offset));
|
|
391
|
+
u.searchParams.set("order_by", "-last_active_at");
|
|
392
|
+
if (options.since) {
|
|
393
|
+
const sinceMs = Date.parse(options.since);
|
|
394
|
+
if (Number.isFinite(sinceMs)) {
|
|
395
|
+
u.searchParams.set("last_active_at_since", String(sinceMs));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return u.toString();
|
|
399
|
+
}
|
|
400
|
+
buildOrganizationsUrl(offset) {
|
|
401
|
+
const u = new URL(`${this.baseUrl()}/v1/organizations`);
|
|
402
|
+
u.searchParams.set("limit", String(PAGE_SIZE));
|
|
403
|
+
u.searchParams.set("offset", String(offset));
|
|
404
|
+
u.searchParams.set("order_by", "-created_at");
|
|
405
|
+
return u.toString();
|
|
406
|
+
}
|
|
407
|
+
buildSessionsUrl(offset) {
|
|
408
|
+
const u = new URL(`${this.baseUrl()}/v1/sessions`);
|
|
409
|
+
u.searchParams.set("limit", String(PAGE_SIZE));
|
|
410
|
+
u.searchParams.set("offset", String(offset));
|
|
411
|
+
return u.toString();
|
|
412
|
+
}
|
|
413
|
+
buildDauUsersUrl(offset) {
|
|
414
|
+
const u = new URL(`${this.baseUrl()}/v1/users`);
|
|
415
|
+
u.searchParams.set("limit", String(PAGE_SIZE));
|
|
416
|
+
u.searchParams.set("offset", String(offset));
|
|
417
|
+
u.searchParams.set("order_by", "-last_active_at");
|
|
418
|
+
u.searchParams.set("last_active_at_since", String(this.dauCutoffMs()));
|
|
419
|
+
return u.toString();
|
|
420
|
+
}
|
|
421
|
+
async fetchUsersPage(page, options, signal) {
|
|
422
|
+
const offset = this.parsePageCursor(page);
|
|
423
|
+
const url = this.buildUsersUrl(offset, options);
|
|
424
|
+
const res = await this.apiGet(url, "users", signal);
|
|
425
|
+
const items = res.body;
|
|
426
|
+
const nextOffset = offset + PAGE_SIZE;
|
|
427
|
+
const pageIndex = Math.floor(offset / PAGE_SIZE);
|
|
428
|
+
const hasMore = items.length >= PAGE_SIZE && pageIndex + 1 < MAX_PAGES;
|
|
429
|
+
return { items, next: hasMore ? String(nextOffset) : null };
|
|
430
|
+
}
|
|
431
|
+
async fetchOrganizationsPage(page, options, signal) {
|
|
432
|
+
const offset = this.parsePageCursor(page);
|
|
433
|
+
const url = this.buildOrganizationsUrl(offset);
|
|
434
|
+
const res = await this.apiGet(
|
|
435
|
+
url,
|
|
436
|
+
"organizations",
|
|
437
|
+
signal
|
|
438
|
+
);
|
|
439
|
+
const { items } = unwrapOrganizations(res.body);
|
|
440
|
+
const sinceMs = options.since ? Date.parse(options.since) : NaN;
|
|
441
|
+
const allOlder = Number.isFinite(sinceMs) && items.length > 0 && items.every((o) => (o.created_at ?? 0) < sinceMs);
|
|
442
|
+
const nextOffset = offset + PAGE_SIZE;
|
|
443
|
+
const pageIndex = Math.floor(offset / PAGE_SIZE);
|
|
444
|
+
const hasMore = items.length >= PAGE_SIZE && !allOlder && pageIndex + 1 < MAX_PAGES;
|
|
445
|
+
return { items, next: hasMore ? String(nextOffset) : null };
|
|
446
|
+
}
|
|
447
|
+
async fetchSessionsPage(page, options, signal) {
|
|
448
|
+
const offset = this.parsePageCursor(page);
|
|
449
|
+
const url = this.buildSessionsUrl(offset);
|
|
450
|
+
const res = await this.apiGet(url, "sessions", signal);
|
|
451
|
+
const items = res.body;
|
|
452
|
+
const sinceMs = options.since ? Date.parse(options.since) : NaN;
|
|
453
|
+
const allOlder = Number.isFinite(sinceMs) && items.length > 0 && items.every((s) => (s.created_at ?? 0) < sinceMs);
|
|
454
|
+
const nextOffset = offset + PAGE_SIZE;
|
|
455
|
+
const pageIndex = Math.floor(offset / PAGE_SIZE);
|
|
456
|
+
const hasMore = items.length >= PAGE_SIZE && !allOlder && pageIndex + 1 < MAX_PAGES;
|
|
457
|
+
return { items, next: hasMore ? String(nextOffset) : null };
|
|
458
|
+
}
|
|
459
|
+
async fetchDauUsersPage(page, signal) {
|
|
460
|
+
const offset = this.parsePageCursor(page);
|
|
461
|
+
const url = this.buildDauUsersUrl(offset);
|
|
462
|
+
const res = await this.apiGet(url, "dau_users", signal);
|
|
463
|
+
const items = res.body;
|
|
464
|
+
const nextOffset = offset + PAGE_SIZE;
|
|
465
|
+
const pageIndex = Math.floor(offset / PAGE_SIZE);
|
|
466
|
+
const hasMore = items.length >= PAGE_SIZE && pageIndex + 1 < MAX_PAGES;
|
|
467
|
+
return { items, next: hasMore ? String(nextOffset) : null };
|
|
468
|
+
}
|
|
469
|
+
async writeUsers(storage, items) {
|
|
470
|
+
for (const u of items) {
|
|
471
|
+
const { email, verified } = primaryEmail(u);
|
|
472
|
+
const lastSignIn = parseEpoch(u.last_sign_in_at ?? null, "ms");
|
|
473
|
+
const lastActive = parseEpoch(u.last_active_at ?? null, "ms");
|
|
474
|
+
const createdAt = parseEpoch(u.created_at ?? null, "ms");
|
|
475
|
+
const updatedAt = parseEpoch(u.updated_at ?? null, "ms");
|
|
476
|
+
await storage.entity({
|
|
477
|
+
type: USER_ENTITY,
|
|
478
|
+
id: u.id,
|
|
479
|
+
attributes: {
|
|
480
|
+
email,
|
|
481
|
+
emailVerified: verified,
|
|
482
|
+
lastSignInAt: lastSignIn,
|
|
483
|
+
lastActiveAt: lastActive,
|
|
484
|
+
banned: u.banned ?? false,
|
|
485
|
+
locked: u.locked ?? false,
|
|
486
|
+
createdAt
|
|
487
|
+
},
|
|
488
|
+
updated_at: updatedAt ?? createdAt ?? 0
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async writeOrganizations(storage, items) {
|
|
493
|
+
for (const o of items) {
|
|
494
|
+
const createdAt = parseEpoch(o.created_at ?? null, "ms");
|
|
495
|
+
const updatedAt = parseEpoch(o.updated_at ?? null, "ms");
|
|
496
|
+
await storage.entity({
|
|
497
|
+
type: ORG_ENTITY,
|
|
498
|
+
id: o.id,
|
|
499
|
+
attributes: {
|
|
500
|
+
name: o.name ?? null,
|
|
501
|
+
slug: o.slug ?? null,
|
|
502
|
+
membersCount: o.members_count ?? null,
|
|
503
|
+
createdAt
|
|
504
|
+
},
|
|
505
|
+
updated_at: updatedAt ?? createdAt ?? 0
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async writeSessions(storage, items) {
|
|
510
|
+
for (const s of items) {
|
|
511
|
+
const startTs = parseEpoch(s.created_at ?? null, "ms");
|
|
512
|
+
if (startTs === null) {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
const status = isSessionStatus(s.status) ? s.status : "active";
|
|
516
|
+
const lastActive = parseEpoch(s.last_active_at ?? null, "ms");
|
|
517
|
+
await storage.event({
|
|
518
|
+
name: SESSION_EVENT,
|
|
519
|
+
start_ts: startTs,
|
|
520
|
+
end_ts: null,
|
|
521
|
+
attributes: {
|
|
522
|
+
sessionId: s.id,
|
|
523
|
+
userId: s.user_id ?? null,
|
|
524
|
+
status,
|
|
525
|
+
lastActiveAt: lastActive
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
accumulateDau(items) {
|
|
531
|
+
const cutoff = this.dauCutoffMs();
|
|
532
|
+
for (const u of items) {
|
|
533
|
+
const ts = u.last_active_at;
|
|
534
|
+
if (typeof ts !== "number" || !Number.isFinite(ts) || ts < cutoff) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const bucket = dayBucket(ts);
|
|
538
|
+
const set = this.dauBuckets.get(bucket) ?? /* @__PURE__ */ new Set();
|
|
539
|
+
set.add(u.id);
|
|
540
|
+
this.dauBuckets.set(bucket, set);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
async writeDauSamples(storage) {
|
|
544
|
+
const samples = Array.from(this.dauBuckets.entries()).sort(([a], [b]) => a - b).map(([ts, set]) => ({
|
|
545
|
+
name: DAU_METRIC,
|
|
546
|
+
ts,
|
|
547
|
+
value: set.size,
|
|
548
|
+
attributes: {}
|
|
549
|
+
}));
|
|
550
|
+
await storage.metrics(samples, { names: [DAU_METRIC] });
|
|
551
|
+
}
|
|
552
|
+
async clearScopeOnFirstPage(storage, phase, isFull) {
|
|
553
|
+
if (phase === "daily_active_users") {
|
|
554
|
+
this.dauBuckets.clear();
|
|
555
|
+
await storage.metrics([], { names: [DAU_METRIC] });
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (!isFull) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
switch (phase) {
|
|
562
|
+
case "users":
|
|
563
|
+
await storage.entities([], { types: [USER_ENTITY] });
|
|
564
|
+
return;
|
|
565
|
+
case "organizations":
|
|
566
|
+
await storage.entities([], { types: [ORG_ENTITY] });
|
|
567
|
+
return;
|
|
568
|
+
case "sessions":
|
|
569
|
+
await storage.events([], { names: [SESSION_EVENT] });
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
resolveCursor(cursor) {
|
|
574
|
+
return isClerkSyncCursor(cursor) ? cursor : void 0;
|
|
575
|
+
}
|
|
576
|
+
async sync(options, storage, signal) {
|
|
577
|
+
const cursor = this.resolveCursor(options.cursor);
|
|
578
|
+
const isFull = options.mode === "full";
|
|
579
|
+
const phases = selectActivePhases(
|
|
580
|
+
(r) => r,
|
|
581
|
+
PHASE_ORDER,
|
|
582
|
+
this.settings.resources
|
|
583
|
+
);
|
|
584
|
+
return paginateChunked({
|
|
585
|
+
phases,
|
|
586
|
+
cursor,
|
|
587
|
+
signal,
|
|
588
|
+
logger: this.logger,
|
|
589
|
+
fetchPage: async (phase, page, sig) => {
|
|
590
|
+
switch (phase) {
|
|
591
|
+
case "users":
|
|
592
|
+
return this.fetchUsersPage(page, options, sig);
|
|
593
|
+
case "organizations":
|
|
594
|
+
return this.fetchOrganizationsPage(page, options, sig);
|
|
595
|
+
case "sessions":
|
|
596
|
+
return this.fetchSessionsPage(page, options, sig);
|
|
597
|
+
case "daily_active_users":
|
|
598
|
+
return this.fetchDauUsersPage(page, sig);
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
writeBatch: async (phase, items, page) => {
|
|
602
|
+
if (page === null) {
|
|
603
|
+
await this.clearScopeOnFirstPage(storage, phase, isFull);
|
|
604
|
+
}
|
|
605
|
+
switch (phase) {
|
|
606
|
+
case "users":
|
|
607
|
+
await this.writeUsers(storage, items);
|
|
608
|
+
return;
|
|
609
|
+
case "organizations":
|
|
610
|
+
await this.writeOrganizations(
|
|
611
|
+
storage,
|
|
612
|
+
items
|
|
613
|
+
);
|
|
614
|
+
return;
|
|
615
|
+
case "sessions":
|
|
616
|
+
await this.writeSessions(storage, items);
|
|
617
|
+
return;
|
|
618
|
+
case "daily_active_users":
|
|
619
|
+
this.accumulateDau(items);
|
|
620
|
+
await this.writeDauSamples(storage);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// src/index.ts
|
|
629
|
+
var index_default = ClerkConnector;
|
|
630
|
+
export {
|
|
631
|
+
ClerkConnector,
|
|
632
|
+
configFields,
|
|
633
|
+
index_default as default,
|
|
634
|
+
doc,
|
|
635
|
+
id,
|
|
636
|
+
clerkResources as resources
|
|
637
|
+
};
|
|
638
|
+
//# sourceMappingURL=index.js.map
|