@emulators/clerk 0.4.1

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/dist/index.js ADDED
@@ -0,0 +1,1822 @@
1
+ import {
2
+ createDefaultEmailAddress,
3
+ createDefaultUser,
4
+ generateClerkId,
5
+ nowUnix,
6
+ userDisplayName
7
+ } from "./chunk-XOGS3H5N.js";
8
+
9
+ // src/routes/oauth.ts
10
+ import { randomBytes } from "crypto";
11
+ import { SignJWT, exportJWK, generateKeyPair } from "jose";
12
+
13
+ // ../core/dist/index.js
14
+ import { Hono } from "hono";
15
+ import { cors } from "hono/cors";
16
+ import { jwtVerify, importPKCS8 } from "jose";
17
+ import { readFileSync } from "fs";
18
+ import { fileURLToPath } from "url";
19
+ import { dirname, join } from "path";
20
+ import { timingSafeEqual } from "crypto";
21
+ function createErrorHandler(documentationUrl) {
22
+ return async (c, next) => {
23
+ if (documentationUrl) {
24
+ c.set("docsUrl", documentationUrl);
25
+ }
26
+ await next();
27
+ };
28
+ }
29
+ var errorHandler = createErrorHandler();
30
+ var isDebug = typeof process !== "undefined" && (process.env.DEBUG === "1" || process.env.DEBUG === "true" || process.env.EMULATE_DEBUG === "1");
31
+ var __dirname = dirname(fileURLToPath(import.meta.url));
32
+ var FONTS = {
33
+ "geist-sans.woff2": readFileSync(join(__dirname, "fonts", "geist-sans.woff2")),
34
+ "GeistPixel-Square.woff2": readFileSync(join(__dirname, "fonts", "GeistPixel-Square.woff2"))
35
+ };
36
+ var FAVICON = readFileSync(join(__dirname, "fonts", "favicon.ico"));
37
+ function escapeHtml(s) {
38
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
39
+ }
40
+ function escapeAttr(s) {
41
+ return escapeHtml(s).replace(/'/g, "&#39;");
42
+ }
43
+ var CSS = `
44
+ @font-face{
45
+ font-family:'Geist';font-style:normal;font-weight:100 900;font-display:swap;
46
+ src:url('/_emulate/fonts/geist-sans.woff2') format('woff2');
47
+ }
48
+ @font-face{
49
+ font-family:'Geist Pixel';font-style:normal;font-weight:400;font-display:swap;
50
+ src:url('/_emulate/fonts/GeistPixel-Square.woff2') format('woff2');
51
+ }
52
+ *{box-sizing:border-box;margin:0;padding:0}
53
+ body{
54
+ font-family:'Geist',-apple-system,BlinkMacSystemFont,sans-serif;
55
+ background:#000;color:#33ff00;min-height:100vh;
56
+ -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
57
+ }
58
+ .emu-bar{
59
+ border-bottom:1px solid #0a3300;padding:10px 20px;
60
+ display:flex;align-items:center;gap:10px;font-size:.8125rem;color:#1a8c00;
61
+ }
62
+ .emu-bar-title{font-weight:600;color:#33ff00;font-family:'Geist Pixel',monospace;}
63
+ .emu-bar-links{margin-left:auto;display:flex;gap:16px;}
64
+ .emu-bar-links a{
65
+ color:#1a8c00;font-size:.75rem;text-decoration:none;transition:color .15s;
66
+ }
67
+ .emu-bar-links a:hover{color:#33ff00;}
68
+ .emu-bar-links a .full{display:inline;}
69
+ .emu-bar-links a .short{display:none;}
70
+ @media(max-width:600px){
71
+ .emu-bar-links a .full{display:none;}
72
+ .emu-bar-links a .short{display:inline;}
73
+ }
74
+
75
+ .content{
76
+ display:flex;align-items:center;justify-content:center;
77
+ min-height:calc(100vh - 42px);padding:24px 16px;
78
+ }
79
+ .content-inner{width:100%;max-width:420px;}
80
+ .card-title{
81
+ font-family:'Geist Pixel',monospace;
82
+ font-size:1.125rem;font-weight:600;margin-bottom:4px;color:#33ff00;
83
+ }
84
+ .card-subtitle{color:#1a8c00;font-size:.8125rem;margin-bottom:18px;line-height:1.45;}
85
+ .powered-by{
86
+ position:fixed;bottom:0;left:0;right:0;
87
+ text-align:center;padding:12px;font-size:.6875rem;color:#0a3300;
88
+ font-family:'Geist Pixel',monospace;
89
+ }
90
+ .powered-by a{color:#1a8c00;text-decoration:none;transition:color .15s;}
91
+ .powered-by a:hover{color:#33ff00;}
92
+
93
+ .error-title{
94
+ font-family:'Geist Pixel',monospace;
95
+ color:#ff4444;font-size:1.125rem;font-weight:600;margin-bottom:8px;
96
+ }
97
+ .error-msg{color:#1a8c00;font-size:.875rem;line-height:1.5;}
98
+ .error-card{text-align:center;}
99
+
100
+ .user-form{margin-bottom:8px;}
101
+ .user-form:last-of-type{margin-bottom:0;}
102
+ .user-btn{
103
+ width:100%;display:flex;align-items:center;gap:12px;
104
+ padding:10px 12px;border:1px solid #0a3300;border-radius:8px;
105
+ background:#000;color:inherit;cursor:pointer;text-align:left;
106
+ font:inherit;transition:border-color .15s;
107
+ }
108
+ .user-btn:hover{border-color:#33ff00;}
109
+ .avatar{
110
+ width:36px;height:36px;border-radius:50%;
111
+ background:#0a3300;color:#33ff00;font-weight:600;font-size:.875rem;
112
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
113
+ font-family:'Geist Pixel',monospace;
114
+ }
115
+ .user-text{min-width:0;}
116
+ .user-login{font-weight:600;font-size:.875rem;display:block;color:#33ff00;}
117
+ .user-meta{color:#1a8c00;font-size:.75rem;margin-top:1px;}
118
+ .user-email{font-size:.6875rem;color:#116600;word-break:break-all;margin-top:1px;}
119
+
120
+ .settings-layout{
121
+ max-width:920px;margin:0 auto;padding:28px 20px;
122
+ display:flex;gap:28px;
123
+ }
124
+ .settings-sidebar{width:200px;flex-shrink:0;}
125
+ .settings-sidebar a{
126
+ display:block;padding:6px 10px;border-radius:6px;color:#1a8c00;
127
+ text-decoration:none;font-size:.8125rem;transition:color .15s;
128
+ }
129
+ .settings-sidebar a:hover{color:#33ff00;}
130
+ .settings-sidebar a.active{color:#33ff00;font-weight:600;}
131
+ .settings-main{flex:1;min-width:0;}
132
+
133
+ .s-card{
134
+ padding:18px 0;margin-bottom:14px;border-bottom:1px solid #0a3300;
135
+ }
136
+ .s-card:last-child{border-bottom:none;}
137
+ .s-card-header{display:flex;align-items:center;gap:14px;margin-bottom:14px;}
138
+ .s-icon{
139
+ width:42px;height:42px;border-radius:8px;
140
+ background:#0a3300;display:flex;align-items:center;justify-content:center;
141
+ font-size:1.125rem;font-weight:700;color:#116600;flex-shrink:0;
142
+ font-family:'Geist Pixel',monospace;
143
+ }
144
+ .s-title{
145
+ font-family:'Geist Pixel',monospace;
146
+ font-size:1.25rem;font-weight:600;color:#33ff00;
147
+ }
148
+ .s-subtitle{font-size:.75rem;color:#1a8c00;margin-top:2px;}
149
+ .section-heading{
150
+ font-size:.9375rem;font-weight:600;margin-bottom:10px;color:#33ff00;
151
+ display:flex;align-items:center;justify-content:space-between;
152
+ }
153
+ .perm-list{list-style:none;}
154
+ .perm-list li{padding:5px 0;font-size:.8125rem;display:flex;align-items:center;gap:6px;color:#1a8c00;}
155
+ .check{color:#33ff00;}
156
+ .org-row{
157
+ display:flex;align-items:center;gap:8px;padding:7px 0;
158
+ border-bottom:1px solid #0a3300;font-size:.8125rem;
159
+ }
160
+ .org-row:last-child{border-bottom:none;}
161
+ .org-icon{
162
+ width:22px;height:22px;border-radius:4px;background:#0a3300;
163
+ display:flex;align-items:center;justify-content:center;
164
+ font-size:.625rem;font-weight:700;color:#116600;flex-shrink:0;
165
+ font-family:'Geist Pixel',monospace;
166
+ }
167
+ .org-name{font-weight:600;color:#33ff00;}
168
+ .badge{font-size:.6875rem;padding:1px 7px;border-radius:999px;font-weight:500;}
169
+ .badge-granted{background:#0a3300;color:#33ff00;}
170
+ .badge-denied{background:#1a0a0a;color:#ff4444;}
171
+ .badge-requested{background:#0a3300;color:#1a8c00;}
172
+ .btn-revoke{
173
+ display:inline-block;padding:5px 14px;border-radius:6px;
174
+ border:1px solid #0a3300;background:transparent;color:#ff4444;
175
+ font-size:.75rem;font-weight:600;cursor:pointer;transition:border-color .15s;
176
+ }
177
+ .btn-revoke:hover{border-color:#ff4444;}
178
+ .info-text{color:#1a8c00;font-size:.75rem;line-height:1.5;margin-top:10px;}
179
+ .app-link{
180
+ display:flex;align-items:center;gap:12px;padding:12px;
181
+ border:1px solid #0a3300;border-radius:8px;background:#000;
182
+ text-decoration:none;color:inherit;margin-bottom:8px;transition:border-color .15s;
183
+ }
184
+ .app-link:hover{border-color:#33ff00;}
185
+ .app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
186
+ .app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
187
+ .empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
188
+
189
+ .inspector-layout{max-width:960px;margin:0 auto;padding:28px 20px;}
190
+ .inspector-tabs{display:flex;gap:4px;margin-bottom:20px;}
191
+ .inspector-tabs a{
192
+ padding:7px 16px;border-radius:6px;text-decoration:none;
193
+ font-size:.8125rem;color:#1a8c00;border:1px solid transparent;
194
+ transition:color .15s,border-color .15s;
195
+ }
196
+ .inspector-tabs a:hover{color:#33ff00;}
197
+ .inspector-tabs a.active{color:#33ff00;font-weight:600;border-color:#0a3300;background:#0a3300;}
198
+ .inspector-section{margin-bottom:24px;}
199
+ .inspector-section h2{
200
+ font-family:'Geist Pixel',monospace;
201
+ font-size:1rem;font-weight:600;color:#33ff00;margin-bottom:10px;
202
+ }
203
+ .inspector-section h3{
204
+ font-family:'Geist Pixel',monospace;
205
+ font-size:.875rem;font-weight:600;color:#1a8c00;margin:16px 0 8px;
206
+ }
207
+ .inspector-table{width:100%;border-collapse:collapse;margin-bottom:12px;}
208
+ .inspector-table th,.inspector-table td{
209
+ text-align:left;padding:8px 12px;border-bottom:1px solid #0a3300;
210
+ font-size:.8125rem;
211
+ }
212
+ .inspector-table th{color:#1a8c00;font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;}
213
+ .inspector-table td{color:#33ff00;}
214
+ .inspector-table tbody tr{transition:background .1s;}
215
+ .inspector-table tbody tr:hover{background:#0a3300;}
216
+ .inspector-empty{color:#1a8c00;text-align:center;padding:20px 0;font-size:.8125rem;}
217
+ `;
218
+ var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
219
+ function emuBar(service) {
220
+ const title = service ? `${escapeHtml(service)} Emulator` : "Emulator";
221
+ return `<div class="emu-bar">
222
+ <span class="emu-bar-title">${title}</span>
223
+ <nav class="emu-bar-links">
224
+ <a href="https://github.com/vercel-labs/emulate/issues" target="_blank" rel="noopener"><span class="full">Report Issue</span><span class="short">Report</span></a>
225
+ <a href="https://github.com/vercel-labs/emulate" target="_blank" rel="noopener"><span class="full">Source Code</span><span class="short">Source</span></a>
226
+ <a href="https://emulate.dev" target="_blank" rel="noopener"><span class="full">Learn More</span><span class="short">Learn</span></a>
227
+ </nav>
228
+ </div>`;
229
+ }
230
+ function head(title) {
231
+ return `<!DOCTYPE html>
232
+ <html lang="en">
233
+ <head>
234
+ <meta charset="utf-8"/>
235
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
236
+ <link rel="icon" href="/_emulate/favicon.ico"/>
237
+ <title>${escapeHtml(title)} | emulate</title>
238
+ <style>${CSS}</style>
239
+ </head>`;
240
+ }
241
+ function renderCardPage(title, subtitle, body, service) {
242
+ return `${head(title)}
243
+ <body>
244
+ ${emuBar(service)}
245
+ <div class="content">
246
+ <div class="content-inner">
247
+ <div class="card-title">${escapeHtml(title)}</div>
248
+ <div class="card-subtitle">${subtitle}</div>
249
+ ${body}
250
+ </div>
251
+ </div>
252
+ ${POWERED_BY}
253
+ </body></html>`;
254
+ }
255
+ function renderErrorPage(title, message, service) {
256
+ return `${head(title)}
257
+ <body>
258
+ ${emuBar(service)}
259
+ <div class="content">
260
+ <div class="content-inner error-card">
261
+ <div class="error-title">${escapeHtml(title)}</div>
262
+ <div class="error-msg">${escapeHtml(message)}</div>
263
+ </div>
264
+ </div>
265
+ ${POWERED_BY}
266
+ </body></html>`;
267
+ }
268
+ function renderUserButton(opts) {
269
+ const hiddens = Object.entries(opts.hiddenFields).map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}"/>`).join("");
270
+ const nameLine = opts.name ? `<div class="user-meta">${escapeHtml(opts.name)}</div>` : "";
271
+ const emailLine = opts.email ? `<div class="user-email">${escapeHtml(opts.email)}</div>` : "";
272
+ return `<form class="user-form" method="post" action="${escapeAttr(opts.formAction)}">
273
+ ${hiddens}
274
+ <button type="submit" class="user-btn">
275
+ <span class="avatar">${escapeHtml(opts.letter)}</span>
276
+ <span class="user-text">
277
+ <span class="user-login">${escapeHtml(opts.login)}</span>
278
+ ${nameLine}${emailLine}
279
+ </span>
280
+ </button>
281
+ </form>`;
282
+ }
283
+ function normalizeUri(uri) {
284
+ try {
285
+ const u = new URL(uri);
286
+ return `${u.origin}${u.pathname.replace(/\/+$/, "")}`;
287
+ } catch {
288
+ return uri.replace(/\/+$/, "").split("?")[0];
289
+ }
290
+ }
291
+ function matchesRedirectUri(incoming, registered) {
292
+ const normalized = normalizeUri(incoming);
293
+ return registered.some((r) => normalizeUri(r) === normalized);
294
+ }
295
+ function constantTimeSecretEqual(a, b) {
296
+ const bufA = Buffer.from(a, "utf-8");
297
+ const bufB = Buffer.from(b, "utf-8");
298
+ if (bufA.length !== bufB.length) return false;
299
+ return timingSafeEqual(bufA, bufB);
300
+ }
301
+ function bodyStr(v) {
302
+ if (typeof v === "string") return v;
303
+ if (Array.isArray(v) && typeof v[0] === "string") return v[0];
304
+ return "";
305
+ }
306
+
307
+ // src/store.ts
308
+ function getClerkStore(store) {
309
+ return {
310
+ users: store.collection("clerk.users", ["clerk_id", "username"]),
311
+ emailAddresses: store.collection("clerk.emails", ["email_id", "user_id", "email_address"]),
312
+ organizations: store.collection("clerk.orgs", ["clerk_id", "slug"]),
313
+ memberships: store.collection("clerk.memberships", [
314
+ "membership_id",
315
+ "org_id",
316
+ "user_id"
317
+ ]),
318
+ invitations: store.collection("clerk.invitations", ["invitation_id", "org_id"]),
319
+ sessions: store.collection("clerk.sessions", ["clerk_id", "user_id"]),
320
+ oauthApps: store.collection("clerk.oauth_apps", ["app_id", "client_id"])
321
+ };
322
+ }
323
+
324
+ // src/routes/oauth.ts
325
+ var keyPairPromise = generateKeyPair("RS256");
326
+ var KID = "emulate-clerk-1";
327
+ var CODE_TTL_MS = 10 * 60 * 1e3;
328
+ function getPendingCodes(store) {
329
+ let map = store.getData("clerk.oauth.pendingCodes");
330
+ if (!map) {
331
+ map = /* @__PURE__ */ new Map();
332
+ store.setData("clerk.oauth.pendingCodes", map);
333
+ }
334
+ return map;
335
+ }
336
+ function isCodeExpired(code) {
337
+ return Date.now() - code.createdAt > CODE_TTL_MS;
338
+ }
339
+ async function createSessionToken(store, user, sessionId, baseUrl, orgId, orgRole, orgSlug, orgPermissions) {
340
+ const { privateKey } = await keyPairPromise;
341
+ const now = Math.floor(Date.now() / 1e3);
342
+ const claims = {
343
+ sid: sessionId
344
+ };
345
+ if (orgId) {
346
+ claims.org_id = orgId;
347
+ claims.org_role = orgRole ?? "org:member";
348
+ claims.org_slug = orgSlug;
349
+ claims.org_permissions = orgPermissions ?? [];
350
+ }
351
+ if (Object.keys(user.public_metadata).length > 0) {
352
+ claims.metadata = user.public_metadata;
353
+ }
354
+ return new SignJWT(claims).setProtectedHeader({ alg: "RS256", kid: KID, typ: "JWT" }).setIssuer(baseUrl).setSubject(user.clerk_id).setIssuedAt(now).setNotBefore(now).setExpirationTime("1h").sign(privateKey);
355
+ }
356
+ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
357
+ const clerkStore = getClerkStore(store);
358
+ const SERVICE_LABEL = "Clerk";
359
+ app.get("/.well-known/openid-configuration", (c) => {
360
+ return c.json({
361
+ issuer: baseUrl,
362
+ authorization_endpoint: `${baseUrl}/oauth/authorize`,
363
+ token_endpoint: `${baseUrl}/oauth/token`,
364
+ userinfo_endpoint: `${baseUrl}/oauth/userinfo`,
365
+ jwks_uri: `${baseUrl}/v1/jwks`,
366
+ response_types_supported: ["code"],
367
+ subject_types_supported: ["public"],
368
+ id_token_signing_alg_values_supported: ["RS256"],
369
+ scopes_supported: ["openid", "profile", "email"],
370
+ token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic"],
371
+ claims_supported: [
372
+ "sub",
373
+ "iss",
374
+ "aud",
375
+ "exp",
376
+ "iat",
377
+ "nbf",
378
+ "azp",
379
+ "sid",
380
+ "org_id",
381
+ "org_role",
382
+ "org_slug",
383
+ "org_permissions"
384
+ ],
385
+ code_challenge_methods_supported: ["plain", "S256"]
386
+ });
387
+ });
388
+ app.get("/v1/jwks", async (c) => {
389
+ const { publicKey } = await keyPairPromise;
390
+ const jwk = await exportJWK(publicKey);
391
+ return c.json({
392
+ keys: [{ ...jwk, kid: KID, use: "sig", alg: "RS256" }]
393
+ });
394
+ });
395
+ app.get("/oauth/authorize", (c) => {
396
+ const clientId = c.req.query("client_id") ?? "";
397
+ const redirectUri = c.req.query("redirect_uri") ?? "";
398
+ const scope = c.req.query("scope") ?? "openid profile email";
399
+ const state = c.req.query("state") ?? "";
400
+ const nonce = c.req.query("nonce") ?? "";
401
+ const responseType = c.req.query("response_type") ?? "code";
402
+ const codeChallenge = c.req.query("code_challenge") ?? "";
403
+ const codeChallengeMethod = c.req.query("code_challenge_method") ?? "";
404
+ if (responseType !== "code") {
405
+ return c.html(
406
+ renderErrorPage("Unsupported response_type", "Only response_type=code is supported.", SERVICE_LABEL),
407
+ 400
408
+ );
409
+ }
410
+ if (!redirectUri) {
411
+ return c.html(
412
+ renderErrorPage("Missing redirect URI", "The redirect_uri parameter is required.", SERVICE_LABEL),
413
+ 400
414
+ );
415
+ }
416
+ const oauthApps = clerkStore.oauthApps.all();
417
+ let appName = "";
418
+ if (oauthApps.length > 0) {
419
+ const oauthApp = oauthApps.find((a) => a.client_id === clientId);
420
+ if (!oauthApp) {
421
+ return c.html(
422
+ renderErrorPage("Application not found", `The client_id '${clientId}' is not registered.`, SERVICE_LABEL),
423
+ 400
424
+ );
425
+ }
426
+ if (!matchesRedirectUri(redirectUri, oauthApp.redirect_uris)) {
427
+ return c.html(
428
+ renderErrorPage(
429
+ "Redirect URI mismatch",
430
+ "The redirect_uri is not registered for this application.",
431
+ SERVICE_LABEL
432
+ ),
433
+ 400
434
+ );
435
+ }
436
+ appName = oauthApp.name;
437
+ }
438
+ const users = clerkStore.users.all();
439
+ const buttons = users.map((user) => {
440
+ const emails = clerkStore.emailAddresses.findBy("user_id", user.clerk_id);
441
+ const primaryEmail = emails.find((e) => e.is_primary) ?? emails[0];
442
+ return renderUserButton({
443
+ letter: ((user.first_name ?? user.username ?? "?")[0] ?? "?").toUpperCase(),
444
+ login: primaryEmail?.email_address ?? user.username ?? user.clerk_id,
445
+ name: userDisplayName(user),
446
+ email: primaryEmail?.email_address ?? "",
447
+ formAction: "/oauth/authorize/callback",
448
+ hiddenFields: {
449
+ user_ref: user.clerk_id,
450
+ redirect_uri: redirectUri,
451
+ scope,
452
+ state,
453
+ nonce,
454
+ client_id: clientId,
455
+ code_challenge: codeChallenge,
456
+ code_challenge_method: codeChallengeMethod
457
+ }
458
+ });
459
+ }).join("\n");
460
+ const subtitle = appName ? `Sign in to <strong>${escapeHtml(appName)}</strong> with your Clerk account.` : "Choose a seeded user to continue.";
461
+ return c.html(
462
+ renderCardPage(
463
+ "Sign in with Clerk",
464
+ subtitle,
465
+ users.length > 0 ? buttons : '<p class="empty">No users in the emulator store.</p>',
466
+ SERVICE_LABEL
467
+ )
468
+ );
469
+ });
470
+ app.post("/oauth/authorize/callback", async (c) => {
471
+ const body = await c.req.parseBody();
472
+ const userRef = bodyStr(body.user_ref);
473
+ const redirectUri = bodyStr(body.redirect_uri);
474
+ const scope = bodyStr(body.scope) || "openid profile email";
475
+ const state = bodyStr(body.state);
476
+ const nonce = bodyStr(body.nonce);
477
+ const clientId = bodyStr(body.client_id);
478
+ const codeChallenge = bodyStr(body.code_challenge);
479
+ const codeChallengeMethod = bodyStr(body.code_challenge_method);
480
+ if (!redirectUri) {
481
+ return c.html(
482
+ renderErrorPage("Missing redirect URI", "The redirect_uri parameter is required.", SERVICE_LABEL),
483
+ 400
484
+ );
485
+ }
486
+ const user = clerkStore.users.findOneBy("clerk_id", userRef);
487
+ if (!user) {
488
+ return c.html(renderErrorPage("Unknown user", "The selected user is not available.", SERVICE_LABEL), 400);
489
+ }
490
+ const oauthApps = clerkStore.oauthApps.all();
491
+ if (oauthApps.length > 0) {
492
+ const oauthApp = oauthApps.find((a) => a.client_id === clientId);
493
+ if (!oauthApp) {
494
+ return c.html(
495
+ renderErrorPage("Application not found", `The client_id '${clientId}' is not registered.`, SERVICE_LABEL),
496
+ 400
497
+ );
498
+ }
499
+ if (!matchesRedirectUri(redirectUri, oauthApp.redirect_uris)) {
500
+ return c.html(
501
+ renderErrorPage(
502
+ "Redirect URI mismatch",
503
+ "The redirect_uri is not registered for this application.",
504
+ SERVICE_LABEL
505
+ ),
506
+ 400
507
+ );
508
+ }
509
+ }
510
+ const code = randomBytes(20).toString("hex");
511
+ getPendingCodes(store).set(code, {
512
+ userClerkId: user.clerk_id,
513
+ scope,
514
+ redirectUri,
515
+ clientId,
516
+ nonce: nonce || null,
517
+ codeChallenge: codeChallenge || null,
518
+ codeChallengeMethod: codeChallengeMethod || null,
519
+ createdAt: Date.now()
520
+ });
521
+ const url = new URL(redirectUri);
522
+ url.searchParams.set("code", code);
523
+ if (state) url.searchParams.set("state", state);
524
+ return c.redirect(url.toString(), 302);
525
+ });
526
+ app.post("/oauth/token", async (c) => {
527
+ const contentType = c.req.header("Content-Type") ?? "";
528
+ let body = {};
529
+ if (contentType.includes("application/json")) {
530
+ try {
531
+ const parsed = await c.req.json();
532
+ for (const [key, value] of Object.entries(parsed)) {
533
+ if (typeof value === "string") body[key] = value;
534
+ }
535
+ } catch {
536
+ body = {};
537
+ }
538
+ } else {
539
+ const raw = await c.req.text();
540
+ body = Object.fromEntries(new URLSearchParams(raw));
541
+ }
542
+ const grantType = body.grant_type ?? "";
543
+ const code = body.code ?? "";
544
+ const redirectUri = body.redirect_uri ?? "";
545
+ const codeVerifier = body.code_verifier;
546
+ let clientId = body.client_id ?? "";
547
+ let clientSecret = body.client_secret ?? "";
548
+ const authHeader = c.req.header("Authorization") ?? "";
549
+ if (authHeader.startsWith("Basic ")) {
550
+ const decoded = Buffer.from(authHeader.slice(6), "base64").toString("utf8");
551
+ const sep = decoded.indexOf(":");
552
+ if (sep !== -1) {
553
+ if (!clientId) clientId = decodeURIComponent(decoded.slice(0, sep));
554
+ if (!clientSecret) clientSecret = decodeURIComponent(decoded.slice(sep + 1));
555
+ }
556
+ }
557
+ if (grantType !== "authorization_code") {
558
+ return c.json(
559
+ { error: "unsupported_grant_type", error_description: "Only authorization_code is supported." },
560
+ 400
561
+ );
562
+ }
563
+ const pending = getPendingCodes(store).get(code);
564
+ if (!pending || isCodeExpired(pending)) {
565
+ if (pending) getPendingCodes(store).delete(code);
566
+ return c.json({ error: "invalid_grant", error_description: "Authorization code is invalid or expired." }, 400);
567
+ }
568
+ if (redirectUri && redirectUri !== pending.redirectUri) {
569
+ return c.json({ error: "invalid_grant", error_description: "redirect_uri does not match." }, 400);
570
+ }
571
+ const oauthApps = clerkStore.oauthApps.all();
572
+ if (oauthApps.length > 0) {
573
+ const oauthApp = oauthApps.find((a) => a.client_id === clientId);
574
+ if (!oauthApp) {
575
+ return c.json({ error: "invalid_client", error_description: "Unknown client." }, 401);
576
+ }
577
+ if (!oauthApp.is_public && !constantTimeSecretEqual(oauthApp.client_secret, clientSecret)) {
578
+ return c.json({ error: "invalid_client", error_description: "Invalid client credentials." }, 401);
579
+ }
580
+ }
581
+ if (pending.codeChallenge !== null) {
582
+ if (!codeVerifier) {
583
+ return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
584
+ }
585
+ const method = (pending.codeChallengeMethod ?? "plain").toLowerCase();
586
+ if (method === "s256") {
587
+ const { createHash } = await import("crypto");
588
+ const expected = createHash("sha256").update(codeVerifier).digest("base64url");
589
+ if (expected !== pending.codeChallenge) {
590
+ return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
591
+ }
592
+ } else if (method === "plain") {
593
+ if (codeVerifier !== pending.codeChallenge) {
594
+ return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
595
+ }
596
+ }
597
+ }
598
+ const user = clerkStore.users.findOneBy("clerk_id", pending.userClerkId);
599
+ if (!user) return c.json({ error: "invalid_grant", error_description: "Unknown user." }, 400);
600
+ getPendingCodes(store).delete(code);
601
+ const { generateClerkId: generateClerkId2, nowUnix: nowUnix2 } = await import("./helpers-LXLP3DFE.js");
602
+ const sessionId = generateClerkId2("sess_");
603
+ const now = nowUnix2();
604
+ clerkStore.sessions.insert({
605
+ clerk_id: sessionId,
606
+ user_id: user.clerk_id,
607
+ client_id: clientId || "default",
608
+ status: "active",
609
+ last_active_at: now,
610
+ expire_at: now + 86400,
611
+ abandon_at: now + 604800,
612
+ created_at_unix: now,
613
+ updated_at_unix: now
614
+ });
615
+ const accessToken = `clerk_${randomBytes(20).toString("base64url")}`;
616
+ tokenMap?.set(accessToken, {
617
+ login: user.clerk_id,
618
+ id: user.id,
619
+ scopes: pending.scope.split(/\s+/).filter(Boolean)
620
+ });
621
+ const { privateKey } = await keyPairPromise;
622
+ const nowSec = Math.floor(Date.now() / 1e3);
623
+ const emails = clerkStore.emailAddresses.findBy("user_id", user.clerk_id);
624
+ const primaryEmail = emails.find((e) => e.is_primary) ?? emails[0];
625
+ const idToken = await new SignJWT({
626
+ sid: sessionId,
627
+ email: primaryEmail?.email_address,
628
+ email_verified: primaryEmail?.verification_status === "verified",
629
+ name: [user.first_name, user.last_name].filter(Boolean).join(" ") || void 0
630
+ }).setProtectedHeader({ alg: "RS256", kid: KID, typ: "JWT" }).setIssuer(baseUrl).setSubject(user.clerk_id).setAudience(clientId || "default").setIssuedAt(nowSec).setExpirationTime("1h").sign(privateKey);
631
+ return c.json({
632
+ token_type: "Bearer",
633
+ expires_in: 3600,
634
+ access_token: accessToken,
635
+ id_token: idToken,
636
+ scope: pending.scope
637
+ });
638
+ });
639
+ app.get("/oauth/userinfo", (c) => {
640
+ const authUser = c.get("authUser");
641
+ if (!authUser) {
642
+ return c.json({ error: "invalid_token", error_description: "The access token is invalid." }, 401);
643
+ }
644
+ const user = clerkStore.users.findOneBy("clerk_id", authUser.login) ?? clerkStore.users.all()[0];
645
+ if (!user) {
646
+ return c.json({ error: "invalid_token", error_description: "User not found." }, 401);
647
+ }
648
+ const emails = clerkStore.emailAddresses.findBy("user_id", user.clerk_id);
649
+ const primaryEmail = emails.find((e) => e.is_primary) ?? emails[0];
650
+ return c.json({
651
+ sub: user.clerk_id,
652
+ name: [user.first_name, user.last_name].filter(Boolean).join(" ") || void 0,
653
+ email: primaryEmail?.email_address,
654
+ email_verified: primaryEmail?.verification_status === "verified",
655
+ picture: user.image_url
656
+ });
657
+ });
658
+ }
659
+
660
+ // src/route-helpers.ts
661
+ function clerkError(c, status, code, message, longMessage, meta) {
662
+ return c.json(
663
+ {
664
+ errors: [
665
+ {
666
+ code,
667
+ message,
668
+ long_message: longMessage ?? message,
669
+ meta: meta ?? {}
670
+ }
671
+ ]
672
+ },
673
+ status
674
+ );
675
+ }
676
+ function requireSecretKey(c, tokenMap) {
677
+ const existing = c.get("authUser");
678
+ if (existing) return existing;
679
+ const authHeader = c.req.header("Authorization") ?? "";
680
+ if (authHeader.startsWith("Bearer ")) {
681
+ const token = authHeader.slice(7).trim();
682
+ if (token.startsWith("sk_test_") || token.startsWith("sk_live_")) {
683
+ const mapped = tokenMap?.get(token);
684
+ if (mapped) {
685
+ c.set("authUser", mapped);
686
+ c.set("authToken", token);
687
+ c.set("authScopes", mapped.scopes);
688
+ return mapped;
689
+ }
690
+ }
691
+ }
692
+ return clerkError(c, 401, "UNAUTHORIZED", "Authentication failed", "Invalid or missing secret key");
693
+ }
694
+ function isAuthResponse(result) {
695
+ return result instanceof Response;
696
+ }
697
+ function deletedResponse(objectType, objectId) {
698
+ return {
699
+ object: "deleted_object",
700
+ id: objectId,
701
+ slug: null,
702
+ deleted: true
703
+ };
704
+ }
705
+ function paginatedResponse(data, totalCount, limit, offset) {
706
+ return {
707
+ data,
708
+ total_count: totalCount,
709
+ has_more: offset + limit < totalCount
710
+ };
711
+ }
712
+ function parsePagination(c) {
713
+ const limit = Math.min(Math.max(Number.parseInt(c.req.query("limit") ?? "10", 10) || 10, 1), 500);
714
+ const offset = Math.max(Number.parseInt(c.req.query("offset") ?? "0", 10) || 0, 0);
715
+ return { limit, offset };
716
+ }
717
+ function userResponse(user, emailAddresses) {
718
+ return {
719
+ id: user.clerk_id,
720
+ object: "user",
721
+ username: user.username,
722
+ first_name: user.first_name,
723
+ last_name: user.last_name,
724
+ image_url: user.image_url ?? `https://img.clerk.com/preview?seed=${user.clerk_id}`,
725
+ profile_image_url: user.profile_image_url ?? `https://img.clerk.com/preview?seed=${user.clerk_id}`,
726
+ has_image: user.image_url !== null,
727
+ primary_email_address_id: user.primary_email_address_id,
728
+ primary_phone_number_id: user.primary_phone_number_id,
729
+ primary_web3_wallet_id: null,
730
+ email_addresses: emailAddresses.map(emailAddressResponse),
731
+ phone_numbers: [],
732
+ web3_wallets: [],
733
+ external_accounts: [],
734
+ saml_accounts: [],
735
+ passkeys: [],
736
+ password_enabled: user.password_enabled,
737
+ totp_enabled: user.totp_enabled,
738
+ backup_code_enabled: user.backup_code_enabled,
739
+ two_factor_enabled: user.two_factor_enabled,
740
+ banned: user.banned,
741
+ locked: user.locked,
742
+ external_id: user.external_id,
743
+ public_metadata: user.public_metadata,
744
+ private_metadata: user.private_metadata,
745
+ unsafe_metadata: user.unsafe_metadata,
746
+ last_sign_in_at: user.last_sign_in_at,
747
+ last_active_at: user.last_active_at,
748
+ created_at: user.created_at_unix,
749
+ updated_at: user.updated_at_unix
750
+ };
751
+ }
752
+ function emailAddressResponse(email) {
753
+ return {
754
+ id: email.email_id,
755
+ object: "email_address",
756
+ email_address: email.email_address,
757
+ reserved: email.reserved,
758
+ verification: {
759
+ status: email.verification_status,
760
+ strategy: email.verification_strategy
761
+ },
762
+ linked_to: [],
763
+ created_at: email.created_at_unix,
764
+ updated_at: email.updated_at_unix
765
+ };
766
+ }
767
+ function organizationResponse(org) {
768
+ return {
769
+ id: org.clerk_id,
770
+ object: "organization",
771
+ name: org.name,
772
+ slug: org.slug,
773
+ image_url: org.image_url,
774
+ has_image: org.image_url !== null,
775
+ members_count: org.members_count,
776
+ pending_invitations_count: org.pending_invitations_count,
777
+ max_allowed_memberships: org.max_allowed_memberships,
778
+ admin_delete_enabled: org.admin_delete_enabled,
779
+ public_metadata: org.public_metadata,
780
+ private_metadata: org.private_metadata,
781
+ created_at: org.created_at_unix,
782
+ updated_at: org.updated_at_unix
783
+ };
784
+ }
785
+ function membershipResponse(membership, org, user, emailAddresses) {
786
+ return {
787
+ id: membership.membership_id,
788
+ object: "organization_membership",
789
+ role: membership.role,
790
+ permissions: membership.permissions,
791
+ public_metadata: membership.public_metadata,
792
+ private_metadata: membership.private_metadata,
793
+ organization: org ? organizationResponse(org) : null,
794
+ public_user_data: user ? {
795
+ user_id: user.clerk_id,
796
+ first_name: user.first_name,
797
+ last_name: user.last_name,
798
+ image_url: user.image_url,
799
+ has_image: user.image_url !== null,
800
+ identifier: emailAddresses.find((e) => e.is_primary)?.email_address ?? user.username ?? user.clerk_id
801
+ } : null,
802
+ created_at: membership.created_at_unix,
803
+ updated_at: membership.updated_at_unix
804
+ };
805
+ }
806
+ function invitationResponse(invitation) {
807
+ return {
808
+ id: invitation.invitation_id,
809
+ object: "organization_invitation",
810
+ email_address: invitation.email_address,
811
+ role: invitation.role,
812
+ status: invitation.status,
813
+ organization_id: invitation.org_id,
814
+ created_at: invitation.created_at_unix,
815
+ updated_at: invitation.updated_at_unix
816
+ };
817
+ }
818
+ function sessionResponse(session) {
819
+ return {
820
+ id: session.clerk_id,
821
+ object: "session",
822
+ user_id: session.user_id,
823
+ client_id: session.client_id,
824
+ status: session.status,
825
+ last_active_at: session.last_active_at,
826
+ expire_at: session.expire_at,
827
+ abandon_at: session.abandon_at,
828
+ created_at: session.created_at_unix,
829
+ updated_at: session.updated_at_unix
830
+ };
831
+ }
832
+ async function readJsonBody(c) {
833
+ try {
834
+ const body = await c.req.json();
835
+ if (body && typeof body === "object") return body;
836
+ return {};
837
+ } catch {
838
+ return {};
839
+ }
840
+ }
841
+
842
+ // src/routes/users.ts
843
+ function userRoutes({ app, store, tokenMap }) {
844
+ const cs = getClerkStore(store);
845
+ app.get("/v1/users", (c) => {
846
+ const auth = requireSecretKey(c, tokenMap);
847
+ if (isAuthResponse(auth)) return auth;
848
+ const { limit, offset } = parsePagination(c);
849
+ const query = c.req.query("query");
850
+ const orderBy = c.req.query("order_by") ?? "-created_at";
851
+ const emailFilter = c.req.queries("email_address");
852
+ let users = cs.users.all();
853
+ if (query) {
854
+ const q = query.toLowerCase();
855
+ users = users.filter((u) => {
856
+ const emails = cs.emailAddresses.findBy("user_id", u.clerk_id);
857
+ return u.first_name?.toLowerCase().includes(q) || u.last_name?.toLowerCase().includes(q) || u.username?.toLowerCase().includes(q) || emails.some((e) => e.email_address.toLowerCase().includes(q));
858
+ });
859
+ }
860
+ if (emailFilter && emailFilter.length > 0) {
861
+ const emailSet = new Set(emailFilter.map((e) => e.toLowerCase()));
862
+ users = users.filter((u) => {
863
+ const emails = cs.emailAddresses.findBy("user_id", u.clerk_id);
864
+ return emails.some((e) => emailSet.has(e.email_address.toLowerCase()));
865
+ });
866
+ }
867
+ const desc = orderBy.startsWith("-");
868
+ const field = orderBy.replace(/^-/, "");
869
+ users.sort((a, b) => {
870
+ const aVal = field === "created_at" ? a.created_at_unix : a.updated_at_unix;
871
+ const bVal = field === "created_at" ? b.created_at_unix : b.updated_at_unix;
872
+ return desc ? bVal - aVal : aVal - bVal;
873
+ });
874
+ const totalCount = users.length;
875
+ const paged = users.slice(offset, offset + limit);
876
+ const data = paged.map((u) => {
877
+ const emails = cs.emailAddresses.findBy("user_id", u.clerk_id);
878
+ return userResponse(u, emails);
879
+ });
880
+ return c.json(paginatedResponse(data, totalCount, limit, offset));
881
+ });
882
+ app.get("/v1/users/count", (c) => {
883
+ const auth = requireSecretKey(c, tokenMap);
884
+ if (isAuthResponse(auth)) return auth;
885
+ return c.json({ object: "total_count", total_count: cs.users.all().length });
886
+ });
887
+ app.get("/v1/users/:userId", (c) => {
888
+ const auth = requireSecretKey(c, tokenMap);
889
+ if (isAuthResponse(auth)) return auth;
890
+ const userId = c.req.param("userId");
891
+ const user = cs.users.findOneBy("clerk_id", userId);
892
+ if (!user) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "User not found");
893
+ const emails = cs.emailAddresses.findBy("user_id", user.clerk_id);
894
+ return c.json(userResponse(user, emails));
895
+ });
896
+ app.post("/v1/users", async (c) => {
897
+ const auth = requireSecretKey(c, tokenMap);
898
+ if (isAuthResponse(auth)) return auth;
899
+ const body = await readJsonBody(c);
900
+ const now = nowUnix();
901
+ const clerkId = generateClerkId("user_");
902
+ const user = cs.users.insert({
903
+ clerk_id: clerkId,
904
+ username: body.username ?? null,
905
+ first_name: body.first_name ?? null,
906
+ last_name: body.last_name ?? null,
907
+ image_url: null,
908
+ profile_image_url: null,
909
+ external_id: body.external_id ?? null,
910
+ primary_email_address_id: null,
911
+ primary_phone_number_id: null,
912
+ password_enabled: typeof body.password === "string" && body.password.length > 0,
913
+ password_hash: body.password ?? null,
914
+ totp_enabled: false,
915
+ backup_code_enabled: false,
916
+ two_factor_enabled: false,
917
+ banned: false,
918
+ locked: false,
919
+ public_metadata: body.public_metadata ?? {},
920
+ private_metadata: body.private_metadata ?? {},
921
+ unsafe_metadata: body.unsafe_metadata ?? {},
922
+ last_active_at: null,
923
+ last_sign_in_at: null,
924
+ created_at_unix: now,
925
+ updated_at_unix: now
926
+ });
927
+ const emailAddr = body.email_address ?? [];
928
+ const emailList = Array.isArray(emailAddr) ? emailAddr : [emailAddr];
929
+ let primaryEmailId = null;
930
+ for (let i = 0; i < emailList.length; i++) {
931
+ const email = cs.emailAddresses.insert({
932
+ email_id: generateClerkId("idn_"),
933
+ email_address: emailList[i],
934
+ user_id: clerkId,
935
+ verification_status: "verified",
936
+ verification_strategy: "email_code",
937
+ is_primary: i === 0,
938
+ reserved: false,
939
+ created_at_unix: now,
940
+ updated_at_unix: now
941
+ });
942
+ if (i === 0) primaryEmailId = email.email_id;
943
+ }
944
+ if (primaryEmailId) {
945
+ cs.users.update(user.id, { primary_email_address_id: primaryEmailId });
946
+ }
947
+ const emails = cs.emailAddresses.findBy("user_id", clerkId);
948
+ const updatedUser = cs.users.findOneBy("clerk_id", clerkId);
949
+ return c.json(userResponse(updatedUser, emails), 200);
950
+ });
951
+ app.patch("/v1/users/:userId", async (c) => {
952
+ const auth = requireSecretKey(c, tokenMap);
953
+ if (isAuthResponse(auth)) return auth;
954
+ const userId = c.req.param("userId");
955
+ const user = cs.users.findOneBy("clerk_id", userId);
956
+ if (!user) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "User not found");
957
+ const body = await readJsonBody(c);
958
+ const now = nowUnix();
959
+ const patch = { updated_at_unix: now };
960
+ if (body.first_name !== void 0) patch.first_name = body.first_name;
961
+ if (body.last_name !== void 0) patch.last_name = body.last_name;
962
+ if (body.username !== void 0) patch.username = body.username;
963
+ if (body.external_id !== void 0) patch.external_id = body.external_id;
964
+ if (body.primary_email_address_id !== void 0)
965
+ patch.primary_email_address_id = body.primary_email_address_id;
966
+ if (body.primary_phone_number_id !== void 0)
967
+ patch.primary_phone_number_id = body.primary_phone_number_id;
968
+ if (body.public_metadata !== void 0) patch.public_metadata = body.public_metadata;
969
+ if (body.private_metadata !== void 0) patch.private_metadata = body.private_metadata;
970
+ if (body.unsafe_metadata !== void 0) patch.unsafe_metadata = body.unsafe_metadata;
971
+ if (typeof body.password === "string") {
972
+ patch.password_enabled = body.password.length > 0;
973
+ patch.password_hash = body.password;
974
+ }
975
+ cs.users.update(user.id, patch);
976
+ const updated = cs.users.findOneBy("clerk_id", userId);
977
+ const emails = cs.emailAddresses.findBy("user_id", userId);
978
+ return c.json(userResponse(updated, emails));
979
+ });
980
+ app.delete("/v1/users/:userId", (c) => {
981
+ const auth = requireSecretKey(c, tokenMap);
982
+ if (isAuthResponse(auth)) return auth;
983
+ const userId = c.req.param("userId");
984
+ const user = cs.users.findOneBy("clerk_id", userId);
985
+ if (!user) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "User not found");
986
+ for (const email of cs.emailAddresses.findBy("user_id", userId)) {
987
+ cs.emailAddresses.delete(email.id);
988
+ }
989
+ for (const membership of cs.memberships.findBy("user_id", userId)) {
990
+ cs.memberships.delete(membership.id);
991
+ const org = cs.organizations.findOneBy("clerk_id", membership.org_id);
992
+ if (org) cs.organizations.update(org.id, { members_count: Math.max(0, org.members_count - 1) });
993
+ }
994
+ for (const session of cs.sessions.findBy("user_id", userId)) {
995
+ cs.sessions.delete(session.id);
996
+ }
997
+ cs.users.delete(user.id);
998
+ return c.json(deletedResponse("user", userId));
999
+ });
1000
+ app.post("/v1/users/:userId/ban", (c) => {
1001
+ const auth = requireSecretKey(c, tokenMap);
1002
+ if (isAuthResponse(auth)) return auth;
1003
+ const userId = c.req.param("userId");
1004
+ const user = cs.users.findOneBy("clerk_id", userId);
1005
+ if (!user) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "User not found");
1006
+ cs.users.update(user.id, { banned: true, updated_at_unix: nowUnix() });
1007
+ const updated = cs.users.findOneBy("clerk_id", userId);
1008
+ const emails = cs.emailAddresses.findBy("user_id", userId);
1009
+ return c.json(userResponse(updated, emails));
1010
+ });
1011
+ app.post("/v1/users/:userId/unban", (c) => {
1012
+ const auth = requireSecretKey(c, tokenMap);
1013
+ if (isAuthResponse(auth)) return auth;
1014
+ const userId = c.req.param("userId");
1015
+ const user = cs.users.findOneBy("clerk_id", userId);
1016
+ if (!user) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "User not found");
1017
+ cs.users.update(user.id, { banned: false, updated_at_unix: nowUnix() });
1018
+ const updated = cs.users.findOneBy("clerk_id", userId);
1019
+ const emails = cs.emailAddresses.findBy("user_id", userId);
1020
+ return c.json(userResponse(updated, emails));
1021
+ });
1022
+ app.post("/v1/users/:userId/lock", (c) => {
1023
+ const auth = requireSecretKey(c, tokenMap);
1024
+ if (isAuthResponse(auth)) return auth;
1025
+ const userId = c.req.param("userId");
1026
+ const user = cs.users.findOneBy("clerk_id", userId);
1027
+ if (!user) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "User not found");
1028
+ cs.users.update(user.id, { locked: true, updated_at_unix: nowUnix() });
1029
+ const updated = cs.users.findOneBy("clerk_id", userId);
1030
+ const emails = cs.emailAddresses.findBy("user_id", userId);
1031
+ return c.json(userResponse(updated, emails));
1032
+ });
1033
+ app.post("/v1/users/:userId/unlock", (c) => {
1034
+ const auth = requireSecretKey(c, tokenMap);
1035
+ if (isAuthResponse(auth)) return auth;
1036
+ const userId = c.req.param("userId");
1037
+ const user = cs.users.findOneBy("clerk_id", userId);
1038
+ if (!user) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "User not found");
1039
+ cs.users.update(user.id, { locked: false, updated_at_unix: nowUnix() });
1040
+ const updated = cs.users.findOneBy("clerk_id", userId);
1041
+ const emails = cs.emailAddresses.findBy("user_id", userId);
1042
+ return c.json(userResponse(updated, emails));
1043
+ });
1044
+ app.patch("/v1/users/:userId/metadata", async (c) => {
1045
+ const auth = requireSecretKey(c, tokenMap);
1046
+ if (isAuthResponse(auth)) return auth;
1047
+ const userId = c.req.param("userId");
1048
+ const user = cs.users.findOneBy("clerk_id", userId);
1049
+ if (!user) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "User not found");
1050
+ const body = await readJsonBody(c);
1051
+ const patch = { updated_at_unix: nowUnix() };
1052
+ if (body.public_metadata !== void 0) {
1053
+ patch.public_metadata = { ...user.public_metadata, ...body.public_metadata };
1054
+ }
1055
+ if (body.private_metadata !== void 0) {
1056
+ patch.private_metadata = { ...user.private_metadata, ...body.private_metadata };
1057
+ }
1058
+ if (body.unsafe_metadata !== void 0) {
1059
+ patch.unsafe_metadata = { ...user.unsafe_metadata, ...body.unsafe_metadata };
1060
+ }
1061
+ cs.users.update(user.id, patch);
1062
+ const updated = cs.users.findOneBy("clerk_id", userId);
1063
+ const emails = cs.emailAddresses.findBy("user_id", userId);
1064
+ return c.json(userResponse(updated, emails));
1065
+ });
1066
+ app.post("/v1/users/:userId/verify_password", async (c) => {
1067
+ const auth = requireSecretKey(c, tokenMap);
1068
+ if (isAuthResponse(auth)) return auth;
1069
+ const userId = c.req.param("userId");
1070
+ const user = cs.users.findOneBy("clerk_id", userId);
1071
+ if (!user) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "User not found");
1072
+ const body = await readJsonBody(c);
1073
+ const password = body.password;
1074
+ const verified = user.password_hash === password;
1075
+ return c.json({ object: "verification", verified });
1076
+ });
1077
+ }
1078
+
1079
+ // src/routes/email-addresses.ts
1080
+ function emailAddressRoutes({ app, store, tokenMap }) {
1081
+ const cs = getClerkStore(store);
1082
+ app.get("/v1/email_addresses/:emailId", (c) => {
1083
+ const auth = requireSecretKey(c, tokenMap);
1084
+ if (isAuthResponse(auth)) return auth;
1085
+ const emailId = c.req.param("emailId");
1086
+ const email = cs.emailAddresses.findOneBy("email_id", emailId);
1087
+ if (!email) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Email address not found");
1088
+ return c.json(emailAddressResponse(email));
1089
+ });
1090
+ app.post("/v1/email_addresses", async (c) => {
1091
+ const auth = requireSecretKey(c, tokenMap);
1092
+ if (isAuthResponse(auth)) return auth;
1093
+ const body = await readJsonBody(c);
1094
+ const userId = body.user_id;
1095
+ const emailAddr = body.email_address;
1096
+ const verified = body.verified ?? false;
1097
+ const primary = body.primary ?? false;
1098
+ if (!userId || !emailAddr) {
1099
+ return clerkError(c, 422, "INVALID_REQUEST_BODY", "user_id and email_address are required");
1100
+ }
1101
+ const user = cs.users.findOneBy("clerk_id", userId);
1102
+ if (!user) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "User not found");
1103
+ const now = nowUnix();
1104
+ const email = cs.emailAddresses.insert({
1105
+ email_id: generateClerkId("idn_"),
1106
+ email_address: emailAddr,
1107
+ user_id: userId,
1108
+ verification_status: verified ? "verified" : "unverified",
1109
+ verification_strategy: "email_code",
1110
+ is_primary: primary,
1111
+ reserved: false,
1112
+ created_at_unix: now,
1113
+ updated_at_unix: now
1114
+ });
1115
+ if (primary) {
1116
+ for (const existing of cs.emailAddresses.findBy("user_id", userId)) {
1117
+ if (existing.email_id !== email.email_id && existing.is_primary) {
1118
+ cs.emailAddresses.update(existing.id, { is_primary: false });
1119
+ }
1120
+ }
1121
+ cs.users.update(user.id, { primary_email_address_id: email.email_id, updated_at_unix: now });
1122
+ }
1123
+ return c.json(emailAddressResponse(email), 200);
1124
+ });
1125
+ app.patch("/v1/email_addresses/:emailId", async (c) => {
1126
+ const auth = requireSecretKey(c, tokenMap);
1127
+ if (isAuthResponse(auth)) return auth;
1128
+ const emailId = c.req.param("emailId");
1129
+ const email = cs.emailAddresses.findOneBy("email_id", emailId);
1130
+ if (!email) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Email address not found");
1131
+ const body = await readJsonBody(c);
1132
+ const now = nowUnix();
1133
+ const patch = { updated_at_unix: now };
1134
+ if (body.verified !== void 0) {
1135
+ patch.verification_status = body.verified ? "verified" : "unverified";
1136
+ }
1137
+ if (body.primary === true) {
1138
+ patch.is_primary = true;
1139
+ for (const existing of cs.emailAddresses.findBy("user_id", email.user_id)) {
1140
+ if (existing.email_id !== emailId && existing.is_primary) {
1141
+ cs.emailAddresses.update(existing.id, { is_primary: false });
1142
+ }
1143
+ }
1144
+ const user = cs.users.findOneBy("clerk_id", email.user_id);
1145
+ if (user) cs.users.update(user.id, { primary_email_address_id: emailId, updated_at_unix: now });
1146
+ }
1147
+ cs.emailAddresses.update(email.id, patch);
1148
+ const updated = cs.emailAddresses.findOneBy("email_id", emailId);
1149
+ return c.json(emailAddressResponse(updated));
1150
+ });
1151
+ app.delete("/v1/email_addresses/:emailId", (c) => {
1152
+ const auth = requireSecretKey(c, tokenMap);
1153
+ if (isAuthResponse(auth)) return auth;
1154
+ const emailId = c.req.param("emailId");
1155
+ const email = cs.emailAddresses.findOneBy("email_id", emailId);
1156
+ if (!email) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Email address not found");
1157
+ cs.emailAddresses.delete(email.id);
1158
+ if (email.is_primary) {
1159
+ const remaining = cs.emailAddresses.findBy("user_id", email.user_id);
1160
+ const user = cs.users.findOneBy("clerk_id", email.user_id);
1161
+ if (user) {
1162
+ const newPrimary = remaining[0];
1163
+ if (newPrimary) {
1164
+ cs.emailAddresses.update(newPrimary.id, { is_primary: true });
1165
+ cs.users.update(user.id, { primary_email_address_id: newPrimary.email_id });
1166
+ } else {
1167
+ cs.users.update(user.id, { primary_email_address_id: null });
1168
+ }
1169
+ }
1170
+ }
1171
+ return c.json(deletedResponse("email_address", emailId));
1172
+ });
1173
+ }
1174
+
1175
+ // src/routes/organizations.ts
1176
+ function organizationRoutes({ app, store, tokenMap }) {
1177
+ const cs = getClerkStore(store);
1178
+ app.get("/v1/organizations", (c) => {
1179
+ const auth = requireSecretKey(c, tokenMap);
1180
+ if (isAuthResponse(auth)) return auth;
1181
+ const { limit, offset } = parsePagination(c);
1182
+ const query = c.req.query("query");
1183
+ let orgs = cs.organizations.all();
1184
+ if (query) {
1185
+ const q = query.toLowerCase();
1186
+ orgs = orgs.filter((o) => o.name.toLowerCase().includes(q) || o.slug.toLowerCase().includes(q));
1187
+ }
1188
+ orgs.sort((a, b) => b.created_at_unix - a.created_at_unix);
1189
+ const totalCount = orgs.length;
1190
+ const paged = orgs.slice(offset, offset + limit);
1191
+ return c.json(paginatedResponse(paged.map(organizationResponse), totalCount, limit, offset));
1192
+ });
1193
+ app.get("/v1/organizations/:orgId", (c) => {
1194
+ const auth = requireSecretKey(c, tokenMap);
1195
+ if (isAuthResponse(auth)) return auth;
1196
+ const orgId = c.req.param("orgId");
1197
+ const org = cs.organizations.findOneBy("clerk_id", orgId) ?? cs.organizations.findOneBy("slug", orgId);
1198
+ if (!org) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Organization not found");
1199
+ return c.json(organizationResponse(org));
1200
+ });
1201
+ app.post("/v1/organizations", async (c) => {
1202
+ const auth = requireSecretKey(c, tokenMap);
1203
+ if (isAuthResponse(auth)) return auth;
1204
+ const body = await readJsonBody(c);
1205
+ const name = body.name;
1206
+ if (!name) return clerkError(c, 422, "INVALID_REQUEST_BODY", "name is required");
1207
+ const slug = body.slug ?? name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1208
+ const now = nowUnix();
1209
+ const org = cs.organizations.insert({
1210
+ clerk_id: generateClerkId("org_"),
1211
+ name,
1212
+ slug,
1213
+ image_url: null,
1214
+ has_logo: false,
1215
+ members_count: 0,
1216
+ pending_invitations_count: 0,
1217
+ public_metadata: body.public_metadata ?? {},
1218
+ private_metadata: body.private_metadata ?? {},
1219
+ max_allowed_memberships: body.max_allowed_memberships ?? null,
1220
+ admin_delete_enabled: body.admin_delete_enabled ?? true,
1221
+ created_at_unix: now,
1222
+ updated_at_unix: now
1223
+ });
1224
+ if (body.created_by) {
1225
+ const userId = body.created_by;
1226
+ const user = cs.users.findOneBy("clerk_id", userId);
1227
+ if (user) {
1228
+ cs.memberships.insert({
1229
+ membership_id: generateClerkId("orgmem_"),
1230
+ org_id: org.clerk_id,
1231
+ user_id: userId,
1232
+ role: "org:admin",
1233
+ permissions: [
1234
+ "org:sys_profile:manage",
1235
+ "org:sys_profile:delete",
1236
+ "org:sys_memberships:read",
1237
+ "org:sys_memberships:manage",
1238
+ "org:sys_domains:read",
1239
+ "org:sys_domains:manage"
1240
+ ],
1241
+ public_metadata: {},
1242
+ private_metadata: {},
1243
+ created_at_unix: now,
1244
+ updated_at_unix: now
1245
+ });
1246
+ cs.organizations.update(org.id, { members_count: 1 });
1247
+ }
1248
+ }
1249
+ const updated = cs.organizations.findOneBy("clerk_id", org.clerk_id);
1250
+ return c.json(organizationResponse(updated), 200);
1251
+ });
1252
+ app.patch("/v1/organizations/:orgId", async (c) => {
1253
+ const auth = requireSecretKey(c, tokenMap);
1254
+ if (isAuthResponse(auth)) return auth;
1255
+ const orgId = c.req.param("orgId");
1256
+ const org = cs.organizations.findOneBy("clerk_id", orgId);
1257
+ if (!org) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Organization not found");
1258
+ const body = await readJsonBody(c);
1259
+ const now = nowUnix();
1260
+ const patch = { updated_at_unix: now };
1261
+ if (body.name !== void 0) patch.name = body.name;
1262
+ if (body.slug !== void 0) patch.slug = body.slug;
1263
+ if (body.public_metadata !== void 0) patch.public_metadata = body.public_metadata;
1264
+ if (body.private_metadata !== void 0) patch.private_metadata = body.private_metadata;
1265
+ if (body.max_allowed_memberships !== void 0) patch.max_allowed_memberships = body.max_allowed_memberships;
1266
+ if (body.admin_delete_enabled !== void 0) patch.admin_delete_enabled = body.admin_delete_enabled;
1267
+ cs.organizations.update(org.id, patch);
1268
+ const updated = cs.organizations.findOneBy("clerk_id", orgId);
1269
+ return c.json(organizationResponse(updated));
1270
+ });
1271
+ app.delete("/v1/organizations/:orgId", (c) => {
1272
+ const auth = requireSecretKey(c, tokenMap);
1273
+ if (isAuthResponse(auth)) return auth;
1274
+ const orgId = c.req.param("orgId");
1275
+ const org = cs.organizations.findOneBy("clerk_id", orgId);
1276
+ if (!org) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Organization not found");
1277
+ for (const m of cs.memberships.findBy("org_id", orgId)) cs.memberships.delete(m.id);
1278
+ for (const inv of cs.invitations.findBy("org_id", orgId)) cs.invitations.delete(inv.id);
1279
+ cs.organizations.delete(org.id);
1280
+ return c.json(deletedResponse("organization", orgId));
1281
+ });
1282
+ app.patch("/v1/organizations/:orgId/metadata", async (c) => {
1283
+ const auth = requireSecretKey(c, tokenMap);
1284
+ if (isAuthResponse(auth)) return auth;
1285
+ const orgId = c.req.param("orgId");
1286
+ const org = cs.organizations.findOneBy("clerk_id", orgId);
1287
+ if (!org) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Organization not found");
1288
+ const body = await readJsonBody(c);
1289
+ const now = nowUnix();
1290
+ const patch = { updated_at_unix: now };
1291
+ if (body.public_metadata !== void 0) {
1292
+ patch.public_metadata = { ...org.public_metadata, ...body.public_metadata };
1293
+ }
1294
+ if (body.private_metadata !== void 0) {
1295
+ patch.private_metadata = { ...org.private_metadata, ...body.private_metadata };
1296
+ }
1297
+ cs.organizations.update(org.id, patch);
1298
+ const updated = cs.organizations.findOneBy("clerk_id", orgId);
1299
+ return c.json(organizationResponse(updated));
1300
+ });
1301
+ }
1302
+
1303
+ // src/routes/memberships.ts
1304
+ function defaultPermissions(role) {
1305
+ if (role === "org:admin") {
1306
+ return [
1307
+ "org:sys_profile:manage",
1308
+ "org:sys_profile:delete",
1309
+ "org:sys_memberships:read",
1310
+ "org:sys_memberships:manage",
1311
+ "org:sys_domains:read",
1312
+ "org:sys_domains:manage"
1313
+ ];
1314
+ }
1315
+ return ["org:sys_memberships:read"];
1316
+ }
1317
+ function membershipRoutes({ app, store, tokenMap }) {
1318
+ const cs = getClerkStore(store);
1319
+ app.get("/v1/organizations/:orgId/memberships", (c) => {
1320
+ const auth = requireSecretKey(c, tokenMap);
1321
+ if (isAuthResponse(auth)) return auth;
1322
+ const orgId = c.req.param("orgId");
1323
+ const org = cs.organizations.findOneBy("clerk_id", orgId);
1324
+ if (!org) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Organization not found");
1325
+ const { limit, offset } = parsePagination(c);
1326
+ const roleFilter = c.req.query("role");
1327
+ let memberships = cs.memberships.findBy("org_id", orgId);
1328
+ if (roleFilter) {
1329
+ memberships = memberships.filter((m) => m.role === roleFilter);
1330
+ }
1331
+ const totalCount = memberships.length;
1332
+ const paged = memberships.slice(offset, offset + limit);
1333
+ const data = paged.map((m) => {
1334
+ const user = cs.users.findOneBy("clerk_id", m.user_id);
1335
+ const emails = user ? cs.emailAddresses.findBy("user_id", user.clerk_id) : [];
1336
+ return membershipResponse(m, org, user, emails);
1337
+ });
1338
+ return c.json(paginatedResponse(data, totalCount, limit, offset));
1339
+ });
1340
+ app.post("/v1/organizations/:orgId/memberships", async (c) => {
1341
+ const auth = requireSecretKey(c, tokenMap);
1342
+ if (isAuthResponse(auth)) return auth;
1343
+ const orgId = c.req.param("orgId");
1344
+ const org = cs.organizations.findOneBy("clerk_id", orgId);
1345
+ if (!org) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Organization not found");
1346
+ const body = await readJsonBody(c);
1347
+ const userId = body.user_id;
1348
+ const role = body.role ?? "org:member";
1349
+ if (!userId) return clerkError(c, 422, "INVALID_REQUEST_BODY", "user_id is required");
1350
+ const user = cs.users.findOneBy("clerk_id", userId);
1351
+ if (!user) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "User not found");
1352
+ const existing = cs.memberships.findBy("org_id", orgId).find((m) => m.user_id === userId);
1353
+ if (existing) return clerkError(c, 422, "DUPLICATE_RECORD", "User is already a member of this organization");
1354
+ const now = nowUnix();
1355
+ const membership = cs.memberships.insert({
1356
+ membership_id: generateClerkId("orgmem_"),
1357
+ org_id: orgId,
1358
+ user_id: userId,
1359
+ role,
1360
+ permissions: defaultPermissions(role),
1361
+ public_metadata: {},
1362
+ private_metadata: {},
1363
+ created_at_unix: now,
1364
+ updated_at_unix: now
1365
+ });
1366
+ cs.organizations.update(org.id, { members_count: org.members_count + 1, updated_at_unix: now });
1367
+ const emails = cs.emailAddresses.findBy("user_id", userId);
1368
+ const updatedOrg = cs.organizations.findOneBy("clerk_id", orgId);
1369
+ return c.json(membershipResponse(membership, updatedOrg, user, emails), 200);
1370
+ });
1371
+ app.patch("/v1/organizations/:orgId/memberships/:userId", async (c) => {
1372
+ const auth = requireSecretKey(c, tokenMap);
1373
+ if (isAuthResponse(auth)) return auth;
1374
+ const orgId = c.req.param("orgId");
1375
+ const userId = c.req.param("userId");
1376
+ const org = cs.organizations.findOneBy("clerk_id", orgId);
1377
+ if (!org) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Organization not found");
1378
+ const membership = cs.memberships.findBy("org_id", orgId).find((m) => m.user_id === userId);
1379
+ if (!membership) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Membership not found");
1380
+ const body = await readJsonBody(c);
1381
+ const now = nowUnix();
1382
+ const patch = { updated_at_unix: now };
1383
+ if (body.role !== void 0) {
1384
+ patch.role = body.role;
1385
+ patch.permissions = defaultPermissions(body.role);
1386
+ }
1387
+ cs.memberships.update(membership.id, patch);
1388
+ const updated = cs.memberships.findBy("org_id", orgId).find((m) => m.user_id === userId);
1389
+ const user = cs.users.findOneBy("clerk_id", userId);
1390
+ const emails = user ? cs.emailAddresses.findBy("user_id", userId) : [];
1391
+ return c.json(membershipResponse(updated, org, user, emails));
1392
+ });
1393
+ app.delete("/v1/organizations/:orgId/memberships/:userId", (c) => {
1394
+ const auth = requireSecretKey(c, tokenMap);
1395
+ if (isAuthResponse(auth)) return auth;
1396
+ const orgId = c.req.param("orgId");
1397
+ const userId = c.req.param("userId");
1398
+ const org = cs.organizations.findOneBy("clerk_id", orgId);
1399
+ if (!org) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Organization not found");
1400
+ const membership = cs.memberships.findBy("org_id", orgId).find((m) => m.user_id === userId);
1401
+ if (!membership) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Membership not found");
1402
+ cs.memberships.delete(membership.id);
1403
+ cs.organizations.update(org.id, { members_count: Math.max(0, org.members_count - 1), updated_at_unix: nowUnix() });
1404
+ return c.json(deletedResponse("organization_membership", membership.membership_id));
1405
+ });
1406
+ app.patch("/v1/organizations/:orgId/memberships/:userId/metadata", async (c) => {
1407
+ const auth = requireSecretKey(c, tokenMap);
1408
+ if (isAuthResponse(auth)) return auth;
1409
+ const orgId = c.req.param("orgId");
1410
+ const userId = c.req.param("userId");
1411
+ const org = cs.organizations.findOneBy("clerk_id", orgId);
1412
+ if (!org) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Organization not found");
1413
+ const membership = cs.memberships.findBy("org_id", orgId).find((m) => m.user_id === userId);
1414
+ if (!membership) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Membership not found");
1415
+ const body = await readJsonBody(c);
1416
+ const now = nowUnix();
1417
+ const patch = { updated_at_unix: now };
1418
+ if (body.public_metadata !== void 0) {
1419
+ patch.public_metadata = { ...membership.public_metadata, ...body.public_metadata };
1420
+ }
1421
+ if (body.private_metadata !== void 0) {
1422
+ patch.private_metadata = {
1423
+ ...membership.private_metadata,
1424
+ ...body.private_metadata
1425
+ };
1426
+ }
1427
+ cs.memberships.update(membership.id, patch);
1428
+ const updated = cs.memberships.findBy("org_id", orgId).find((m) => m.user_id === userId);
1429
+ const user = cs.users.findOneBy("clerk_id", userId);
1430
+ const emails = user ? cs.emailAddresses.findBy("user_id", userId) : [];
1431
+ return c.json(membershipResponse(updated, org, user, emails));
1432
+ });
1433
+ }
1434
+
1435
+ // src/routes/invitations.ts
1436
+ function invitationRoutes({ app, store, tokenMap }) {
1437
+ const cs = getClerkStore(store);
1438
+ app.get("/v1/organizations/:orgId/invitations", (c) => {
1439
+ const auth = requireSecretKey(c, tokenMap);
1440
+ if (isAuthResponse(auth)) return auth;
1441
+ const orgId = c.req.param("orgId");
1442
+ const org = cs.organizations.findOneBy("clerk_id", orgId);
1443
+ if (!org) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Organization not found");
1444
+ const { limit, offset } = parsePagination(c);
1445
+ const statusFilter = c.req.query("status");
1446
+ let invitations = cs.invitations.findBy("org_id", orgId);
1447
+ if (statusFilter) {
1448
+ invitations = invitations.filter((inv) => inv.status === statusFilter);
1449
+ }
1450
+ const totalCount = invitations.length;
1451
+ const paged = invitations.slice(offset, offset + limit);
1452
+ return c.json(paginatedResponse(paged.map(invitationResponse), totalCount, limit, offset));
1453
+ });
1454
+ app.get("/v1/organizations/:orgId/invitations/:invitationId", (c) => {
1455
+ const auth = requireSecretKey(c, tokenMap);
1456
+ if (isAuthResponse(auth)) return auth;
1457
+ const orgId = c.req.param("orgId");
1458
+ const invitationId = c.req.param("invitationId");
1459
+ const org = cs.organizations.findOneBy("clerk_id", orgId);
1460
+ if (!org) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Organization not found");
1461
+ const invitation = cs.invitations.findOneBy("invitation_id", invitationId);
1462
+ if (!invitation || invitation.org_id !== orgId) {
1463
+ return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Invitation not found");
1464
+ }
1465
+ return c.json(invitationResponse(invitation));
1466
+ });
1467
+ app.post("/v1/organizations/:orgId/invitations", async (c) => {
1468
+ const auth = requireSecretKey(c, tokenMap);
1469
+ if (isAuthResponse(auth)) return auth;
1470
+ const orgId = c.req.param("orgId");
1471
+ const org = cs.organizations.findOneBy("clerk_id", orgId);
1472
+ if (!org) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Organization not found");
1473
+ const body = await readJsonBody(c);
1474
+ const emailAddress = body.email_address;
1475
+ if (!emailAddress) return clerkError(c, 422, "INVALID_REQUEST_BODY", "email_address is required");
1476
+ const role = body.role ?? "org:member";
1477
+ const expiresInDays = body.expires_in_days ?? 30;
1478
+ const now = nowUnix();
1479
+ const invitation = cs.invitations.insert({
1480
+ invitation_id: generateClerkId("orginv_"),
1481
+ email_address: emailAddress,
1482
+ org_id: orgId,
1483
+ role,
1484
+ status: "pending",
1485
+ expires_at: now + expiresInDays * 86400,
1486
+ created_at_unix: now,
1487
+ updated_at_unix: now
1488
+ });
1489
+ cs.organizations.update(org.id, {
1490
+ pending_invitations_count: org.pending_invitations_count + 1,
1491
+ updated_at_unix: now
1492
+ });
1493
+ return c.json(invitationResponse(invitation), 200);
1494
+ });
1495
+ app.post("/v1/organizations/:orgId/invitations/bulk", async (c) => {
1496
+ const auth = requireSecretKey(c, tokenMap);
1497
+ if (isAuthResponse(auth)) return auth;
1498
+ const orgId = c.req.param("orgId");
1499
+ const org = cs.organizations.findOneBy("clerk_id", orgId);
1500
+ if (!org) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Organization not found");
1501
+ const body = await readJsonBody(c);
1502
+ const emailAddresses = body.email_addresses;
1503
+ if (!emailAddresses || !Array.isArray(emailAddresses)) {
1504
+ return clerkError(c, 422, "INVALID_REQUEST_BODY", "email_addresses array is required");
1505
+ }
1506
+ const role = body.role ?? "org:member";
1507
+ const expiresInDays = body.expires_in_days ?? 30;
1508
+ const now = nowUnix();
1509
+ const created = emailAddresses.map(
1510
+ (email) => cs.invitations.insert({
1511
+ invitation_id: generateClerkId("orginv_"),
1512
+ email_address: email,
1513
+ org_id: orgId,
1514
+ role,
1515
+ status: "pending",
1516
+ expires_at: now + expiresInDays * 86400,
1517
+ created_at_unix: now,
1518
+ updated_at_unix: now
1519
+ })
1520
+ );
1521
+ cs.organizations.update(org.id, {
1522
+ pending_invitations_count: org.pending_invitations_count + created.length,
1523
+ updated_at_unix: now
1524
+ });
1525
+ return c.json(created.map(invitationResponse));
1526
+ });
1527
+ app.post("/v1/organizations/:orgId/invitations/:invitationId/revoke", (c) => {
1528
+ const auth = requireSecretKey(c, tokenMap);
1529
+ if (isAuthResponse(auth)) return auth;
1530
+ const orgId = c.req.param("orgId");
1531
+ const invitationId = c.req.param("invitationId");
1532
+ const org = cs.organizations.findOneBy("clerk_id", orgId);
1533
+ if (!org) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Organization not found");
1534
+ const invitation = cs.invitations.findOneBy("invitation_id", invitationId);
1535
+ if (!invitation || invitation.org_id !== orgId) {
1536
+ return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Invitation not found");
1537
+ }
1538
+ if (invitation.status !== "pending") {
1539
+ return clerkError(c, 422, "INVALID_REQUEST_BODY", "Only pending invitations can be revoked");
1540
+ }
1541
+ const now = nowUnix();
1542
+ cs.invitations.update(invitation.id, { status: "revoked", updated_at_unix: now });
1543
+ cs.organizations.update(org.id, {
1544
+ pending_invitations_count: Math.max(0, org.pending_invitations_count - 1),
1545
+ updated_at_unix: now
1546
+ });
1547
+ const updated = cs.invitations.findOneBy("invitation_id", invitationId);
1548
+ return c.json(invitationResponse(updated));
1549
+ });
1550
+ }
1551
+
1552
+ // src/routes/sessions.ts
1553
+ function sessionRoutes({ app, store, baseUrl, tokenMap }) {
1554
+ const cs = getClerkStore(store);
1555
+ app.get("/v1/sessions", (c) => {
1556
+ const auth = requireSecretKey(c, tokenMap);
1557
+ if (isAuthResponse(auth)) return auth;
1558
+ const { limit, offset } = parsePagination(c);
1559
+ const userIdFilter = c.req.query("user_id");
1560
+ let sessions = cs.sessions.all();
1561
+ if (userIdFilter) {
1562
+ sessions = sessions.filter((s) => s.user_id === userIdFilter);
1563
+ }
1564
+ sessions.sort((a, b) => b.created_at_unix - a.created_at_unix);
1565
+ const totalCount = sessions.length;
1566
+ const paged = sessions.slice(offset, offset + limit);
1567
+ return c.json(paginatedResponse(paged.map(sessionResponse), totalCount, limit, offset));
1568
+ });
1569
+ app.get("/v1/sessions/:sessionId", (c) => {
1570
+ const auth = requireSecretKey(c, tokenMap);
1571
+ if (isAuthResponse(auth)) return auth;
1572
+ const sessionId = c.req.param("sessionId");
1573
+ const session = cs.sessions.findOneBy("clerk_id", sessionId);
1574
+ if (!session) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Session not found");
1575
+ return c.json(sessionResponse(session));
1576
+ });
1577
+ app.post("/v1/sessions", async (c) => {
1578
+ const auth = requireSecretKey(c, tokenMap);
1579
+ if (isAuthResponse(auth)) return auth;
1580
+ const body = await readJsonBody(c);
1581
+ const userId = body.user_id;
1582
+ if (!userId) return clerkError(c, 422, "INVALID_REQUEST_BODY", "user_id is required");
1583
+ const user = cs.users.findOneBy("clerk_id", userId);
1584
+ if (!user) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "User not found");
1585
+ const now = nowUnix();
1586
+ const session = cs.sessions.insert({
1587
+ clerk_id: generateClerkId("sess_"),
1588
+ user_id: userId,
1589
+ client_id: body.client_id ?? "client_emulate",
1590
+ status: "active",
1591
+ last_active_at: now,
1592
+ expire_at: now + 86400,
1593
+ abandon_at: now + 604800,
1594
+ created_at_unix: now,
1595
+ updated_at_unix: now
1596
+ });
1597
+ cs.users.update(user.id, { last_active_at: now, last_sign_in_at: now, updated_at_unix: now });
1598
+ return c.json(sessionResponse(session), 200);
1599
+ });
1600
+ app.post("/v1/sessions/:sessionId/revoke", (c) => {
1601
+ const auth = requireSecretKey(c, tokenMap);
1602
+ if (isAuthResponse(auth)) return auth;
1603
+ const sessionId = c.req.param("sessionId");
1604
+ const session = cs.sessions.findOneBy("clerk_id", sessionId);
1605
+ if (!session) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Session not found");
1606
+ cs.sessions.update(session.id, { status: "revoked", updated_at_unix: nowUnix() });
1607
+ const updated = cs.sessions.findOneBy("clerk_id", sessionId);
1608
+ return c.json(sessionResponse(updated));
1609
+ });
1610
+ app.post("/v1/sessions/:sessionId/tokens", async (c) => {
1611
+ const auth = requireSecretKey(c, tokenMap);
1612
+ if (isAuthResponse(auth)) return auth;
1613
+ const sessionId = c.req.param("sessionId");
1614
+ const session = cs.sessions.findOneBy("clerk_id", sessionId);
1615
+ if (!session) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Session not found");
1616
+ if (session.status !== "active") {
1617
+ return clerkError(c, 422, "SESSION_NOT_ACTIVE", "Session is not active");
1618
+ }
1619
+ const user = cs.users.findOneBy("clerk_id", session.user_id);
1620
+ if (!user) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "User not found");
1621
+ const memberships = cs.memberships.findBy("user_id", user.clerk_id);
1622
+ const firstMembership = memberships[0];
1623
+ let orgId;
1624
+ let orgRole;
1625
+ let orgSlug;
1626
+ let orgPermissions;
1627
+ if (firstMembership) {
1628
+ const org = cs.organizations.findOneBy("clerk_id", firstMembership.org_id);
1629
+ if (org) {
1630
+ orgId = org.clerk_id;
1631
+ orgRole = firstMembership.role;
1632
+ orgSlug = org.slug;
1633
+ orgPermissions = firstMembership.permissions;
1634
+ }
1635
+ }
1636
+ const jwt = await createSessionToken(store, user, sessionId, baseUrl, orgId, orgRole, orgSlug, orgPermissions);
1637
+ cs.sessions.update(session.id, { last_active_at: nowUnix() });
1638
+ return c.json({ object: "token", jwt });
1639
+ });
1640
+ app.post("/v1/sessions/:sessionId/tokens/:template", async (c) => {
1641
+ const auth = requireSecretKey(c, tokenMap);
1642
+ if (isAuthResponse(auth)) return auth;
1643
+ const sessionId = c.req.param("sessionId");
1644
+ const session = cs.sessions.findOneBy("clerk_id", sessionId);
1645
+ if (!session) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "Session not found");
1646
+ if (session.status !== "active") {
1647
+ return clerkError(c, 422, "SESSION_NOT_ACTIVE", "Session is not active");
1648
+ }
1649
+ const user = cs.users.findOneBy("clerk_id", session.user_id);
1650
+ if (!user) return clerkError(c, 404, "RESOURCE_NOT_FOUND", "User not found");
1651
+ const jwt = await createSessionToken(store, user, sessionId, baseUrl);
1652
+ cs.sessions.update(session.id, { last_active_at: nowUnix() });
1653
+ return c.json({ object: "token", jwt });
1654
+ });
1655
+ }
1656
+
1657
+ // src/index.ts
1658
+ function seedDefaults(store, _baseUrl) {
1659
+ const cs = getClerkStore(store);
1660
+ if (cs.users.all().length > 0) return;
1661
+ const userInput = createDefaultUser();
1662
+ const user = cs.users.insert(userInput);
1663
+ const email = cs.emailAddresses.insert(createDefaultEmailAddress(user.clerk_id, "test@example.com", true));
1664
+ cs.users.update(user.id, { primary_email_address_id: email.email_id });
1665
+ const now = nowUnix();
1666
+ cs.oauthApps.insert({
1667
+ app_id: generateClerkId("oauth_app_"),
1668
+ name: "Emulate App",
1669
+ client_id: "clerk_emulate_client",
1670
+ client_secret: "clerk_emulate_secret",
1671
+ is_public: false,
1672
+ scopes: ["openid", "profile", "email"],
1673
+ redirect_uris: ["http://localhost:3000/api/auth/callback/clerk"],
1674
+ created_at_unix: now,
1675
+ updated_at_unix: now
1676
+ });
1677
+ }
1678
+ function seedFromConfig(store, _baseUrl, config) {
1679
+ const cs = getClerkStore(store);
1680
+ const now = nowUnix();
1681
+ if (config.users) {
1682
+ for (const userCfg of config.users) {
1683
+ const existingEmail = userCfg.email_addresses?.[0];
1684
+ if (existingEmail) {
1685
+ const found = cs.emailAddresses.findOneBy("email_address", existingEmail);
1686
+ if (found) continue;
1687
+ }
1688
+ const clerkId = userCfg.clerk_id ?? generateClerkId("user_");
1689
+ const user = cs.users.insert({
1690
+ clerk_id: clerkId,
1691
+ username: userCfg.username ?? null,
1692
+ first_name: userCfg.first_name ?? "Test",
1693
+ last_name: userCfg.last_name ?? "User",
1694
+ image_url: null,
1695
+ profile_image_url: null,
1696
+ external_id: userCfg.external_id ?? null,
1697
+ primary_email_address_id: null,
1698
+ primary_phone_number_id: null,
1699
+ password_enabled: typeof userCfg.password === "string" && userCfg.password.length > 0,
1700
+ password_hash: userCfg.password ?? null,
1701
+ totp_enabled: false,
1702
+ backup_code_enabled: false,
1703
+ two_factor_enabled: false,
1704
+ banned: false,
1705
+ locked: false,
1706
+ public_metadata: userCfg.public_metadata ?? {},
1707
+ private_metadata: userCfg.private_metadata ?? {},
1708
+ unsafe_metadata: userCfg.unsafe_metadata ?? {},
1709
+ last_active_at: null,
1710
+ last_sign_in_at: null,
1711
+ created_at_unix: now,
1712
+ updated_at_unix: now
1713
+ });
1714
+ let primaryEmailId = null;
1715
+ if (userCfg.email_addresses) {
1716
+ for (let i = 0; i < userCfg.email_addresses.length; i++) {
1717
+ const email = cs.emailAddresses.insert(
1718
+ createDefaultEmailAddress(clerkId, userCfg.email_addresses[i], i === 0)
1719
+ );
1720
+ if (i === 0) primaryEmailId = email.email_id;
1721
+ }
1722
+ }
1723
+ if (primaryEmailId) {
1724
+ cs.users.update(user.id, { primary_email_address_id: primaryEmailId });
1725
+ }
1726
+ }
1727
+ }
1728
+ if (config.organizations) {
1729
+ for (const orgCfg of config.organizations) {
1730
+ const existingSlug = orgCfg.slug ?? orgCfg.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1731
+ const existing = cs.organizations.findOneBy("slug", existingSlug);
1732
+ if (existing) continue;
1733
+ const orgId = orgCfg.clerk_id ?? generateClerkId("org_");
1734
+ const org = cs.organizations.insert({
1735
+ clerk_id: orgId,
1736
+ name: orgCfg.name,
1737
+ slug: existingSlug,
1738
+ image_url: null,
1739
+ has_logo: false,
1740
+ members_count: 0,
1741
+ pending_invitations_count: 0,
1742
+ public_metadata: orgCfg.public_metadata ?? {},
1743
+ private_metadata: orgCfg.private_metadata ?? {},
1744
+ max_allowed_memberships: orgCfg.max_allowed_memberships ?? null,
1745
+ admin_delete_enabled: true,
1746
+ created_at_unix: now,
1747
+ updated_at_unix: now
1748
+ });
1749
+ if (orgCfg.members) {
1750
+ let memberCount = 0;
1751
+ for (const memberCfg of orgCfg.members) {
1752
+ const emailEntry = cs.emailAddresses.findOneBy("email_address", memberCfg.email);
1753
+ if (!emailEntry) continue;
1754
+ const user = cs.users.findOneBy("clerk_id", emailEntry.user_id);
1755
+ if (!user) continue;
1756
+ const existingMembership = cs.memberships.findBy("org_id", orgId).find((m) => m.user_id === user.clerk_id);
1757
+ if (existingMembership) continue;
1758
+ const role = memberCfg.role.startsWith("org:") ? memberCfg.role : `org:${memberCfg.role}`;
1759
+ cs.memberships.insert({
1760
+ membership_id: generateClerkId("orgmem_"),
1761
+ org_id: orgId,
1762
+ user_id: user.clerk_id,
1763
+ role,
1764
+ permissions: role === "org:admin" ? [
1765
+ "org:sys_profile:manage",
1766
+ "org:sys_profile:delete",
1767
+ "org:sys_memberships:read",
1768
+ "org:sys_memberships:manage"
1769
+ ] : ["org:sys_memberships:read"],
1770
+ public_metadata: {},
1771
+ private_metadata: {},
1772
+ created_at_unix: now,
1773
+ updated_at_unix: now
1774
+ });
1775
+ memberCount++;
1776
+ }
1777
+ cs.organizations.update(org.id, { members_count: memberCount });
1778
+ }
1779
+ }
1780
+ }
1781
+ if (config.oauth_applications) {
1782
+ for (const appCfg of config.oauth_applications) {
1783
+ const existing = cs.oauthApps.findOneBy("client_id", appCfg.client_id);
1784
+ if (existing) continue;
1785
+ cs.oauthApps.insert({
1786
+ app_id: generateClerkId("oauth_app_"),
1787
+ name: appCfg.name,
1788
+ client_id: appCfg.client_id,
1789
+ client_secret: appCfg.client_secret ?? "",
1790
+ is_public: appCfg.public ?? false,
1791
+ scopes: appCfg.scopes ?? ["openid", "profile", "email"],
1792
+ redirect_uris: appCfg.redirect_uris,
1793
+ created_at_unix: now,
1794
+ updated_at_unix: now
1795
+ });
1796
+ }
1797
+ }
1798
+ }
1799
+ var clerkPlugin = {
1800
+ name: "clerk",
1801
+ register(app, store, webhooks, baseUrl, tokenMap) {
1802
+ const ctx = { app, store, webhooks, baseUrl, tokenMap };
1803
+ oauthRoutes(ctx);
1804
+ userRoutes(ctx);
1805
+ emailAddressRoutes(ctx);
1806
+ organizationRoutes(ctx);
1807
+ membershipRoutes(ctx);
1808
+ invitationRoutes(ctx);
1809
+ sessionRoutes(ctx);
1810
+ },
1811
+ seed(store, baseUrl) {
1812
+ seedDefaults(store, baseUrl);
1813
+ }
1814
+ };
1815
+ var index_default = clerkPlugin;
1816
+ export {
1817
+ clerkPlugin,
1818
+ index_default as default,
1819
+ getClerkStore,
1820
+ seedFromConfig
1821
+ };
1822
+ //# sourceMappingURL=index.js.map