@holo-js/auth-clerk 0.1.4 → 0.1.6

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +123 -26
  2. package/dist/index.mjs +748 -244
  3. package/package.json +6 -5
package/dist/index.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  // src/index.ts
2
- import { createPublicKey, verify as verifySignature } from "crypto";
3
- import { authRuntimeInternals } from "@holo-js/auth";
2
+ import { authRuntimeInternals, getAuthRuntime } from "@holo-js/auth";
4
3
  import { parseCookieHeader } from "@holo-js/session";
4
+
5
+ // src/contracts.ts
5
6
  var ClerkAuthConflictError = class extends Error {
6
7
  code = "clerk_identity_conflict";
7
8
  provider;
@@ -15,23 +16,11 @@ var ClerkAuthConflictError = class extends Error {
15
16
  this.email = options.email;
16
17
  }
17
18
  };
18
- var clerkBindings;
19
+
20
+ // src/jwt.ts
21
+ import { createPublicKey, verify as verifySignature } from "crypto";
19
22
  var CLERK_API_BASE_URL = "https://api.clerk.com";
20
- var clerkDefaultProviderRuntimeCache = /* @__PURE__ */ new Map();
21
23
  var clerkJwksCache = /* @__PURE__ */ new Map();
22
- var AUTH_PROVIDER_MARKER = /* @__PURE__ */ Symbol.for("holo-js.auth.provider");
23
- function throwUnconfigured() {
24
- throw new Error("[@holo-js/auth-clerk] Clerk auth runtime is not configured yet.");
25
- }
26
- function getBindings() {
27
- if (!clerkBindings?.identityStore) {
28
- throwUnconfigured();
29
- }
30
- return {
31
- providers: clerkBindings.providers ?? {},
32
- identityStore: clerkBindings.identityStore
33
- };
34
- }
35
24
  function decodeJwtSegment(value, label) {
36
25
  try {
37
26
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
@@ -65,20 +54,6 @@ function verifyJwtSignatureWithJwk(token, jwk) {
65
54
  throw new Error(`[@holo-js/auth-clerk] Unsupported Clerk JWT algorithm "${algorithm || "unknown"}".`);
66
55
  }
67
56
  }
68
- function verifyJwtSignatureWithPem(token, pem) {
69
- const algorithm = typeof token.header.alg === "string" ? token.header.alg : "";
70
- const key = createPublicKey(pem.replace(/\\n/g, "\n"));
71
- switch (algorithm) {
72
- case "RS256":
73
- return verifySignature("RSA-SHA256", token.signingInput, key, token.signature);
74
- case "RS384":
75
- return verifySignature("RSA-SHA384", token.signingInput, key, token.signature);
76
- case "RS512":
77
- return verifySignature("RSA-SHA512", token.signingInput, key, token.signature);
78
- default:
79
- throw new Error(`[@holo-js/auth-clerk] Unsupported Clerk JWT algorithm "${algorithm || "unknown"}".`);
80
- }
81
- }
82
57
  function resolveClerkJwksUrl(config) {
83
58
  const frontendApi = config.frontendApi?.trim();
84
59
  if (frontendApi) {
@@ -115,47 +90,170 @@ async function fetchClerkJwks(jwksUrl, options = {}) {
115
90
  throw error;
116
91
  }
117
92
  }
118
- async function verifyClerkSessionToken(token, config, authorizedParties = []) {
119
- const parsed = parseJwt(token);
120
- const verified = (() => {
121
- const pem = config.jwtKey?.trim();
122
- if (pem) {
123
- return verifyJwtSignatureWithPem(parsed, pem);
93
+
94
+ // src/index.ts
95
+ var CLERK_RUNTIME_STATE_KEY = "__holoJsAuthClerkRuntime";
96
+ var clerkDefaultProviderRuntimeCache = /* @__PURE__ */ new Map();
97
+ var clerkIdentitySyncLocks = /* @__PURE__ */ new Map();
98
+ var AUTH_PROVIDER_MARKER = /* @__PURE__ */ Symbol.for("holo-js.auth.provider");
99
+ function getRuntimeState() {
100
+ const runtimeGlobal = globalThis;
101
+ const runtimeHost = runtimeGlobal.process ?? runtimeGlobal;
102
+ if (!runtimeHost[CLERK_RUNTIME_STATE_KEY]) {
103
+ Object.defineProperty(runtimeHost, CLERK_RUNTIME_STATE_KEY, {
104
+ value: {},
105
+ enumerable: false,
106
+ configurable: true,
107
+ writable: true
108
+ });
109
+ }
110
+ return runtimeHost[CLERK_RUNTIME_STATE_KEY];
111
+ }
112
+ function throwUnconfigured() {
113
+ throw new Error("[@holo-js/auth-clerk] Clerk auth runtime is not configured yet.");
114
+ }
115
+ function isPlainHeaderRecord(value) {
116
+ return Boolean(value) && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype;
117
+ }
118
+ function appendKnownHeaders(headers, input) {
119
+ for (const name of ["authorization", "cookie", "host", "x-forwarded-host", "x-forwarded-proto"]) {
120
+ const value = input.get?.(name);
121
+ if (typeof value === "string" && value) {
122
+ headers.set(name, value);
124
123
  }
125
- return void 0;
126
- })();
127
- if (verified === false) {
128
- throw new Error("[@holo-js/auth-clerk] Clerk token signature verification failed.");
129
124
  }
130
- if (typeof verified === "undefined") {
131
- const headerKid = typeof parsed.header.kid === "string" ? parsed.header.kid : void 0;
132
- const jwksUrl = resolveClerkJwksUrl(config);
133
- const resolveKey = async (refresh = false) => {
134
- const keys = await fetchClerkJwks(jwksUrl, { refresh });
135
- return headerKid ? keys.find((candidate) => candidate.kid === headerKid) : keys[0];
136
- };
137
- let key = await resolveKey();
138
- if (!key || !verifyJwtSignatureWithJwk(parsed, key)) {
139
- key = await resolveKey(true);
125
+ }
126
+ function hasHeaderForEach(input) {
127
+ return !Array.isArray(input) && "forEach" in input && typeof input.forEach === "function";
128
+ }
129
+ function hasHeaderEntries(input) {
130
+ return !Array.isArray(input) && "entries" in input && typeof input.entries === "function";
131
+ }
132
+ function hasHeaderGet(input) {
133
+ return !Array.isArray(input) && "get" in input && typeof input.get === "function";
134
+ }
135
+ function normalizeRequestHeaders(input) {
136
+ const headers = new Headers();
137
+ if (!input) {
138
+ return headers;
139
+ }
140
+ if (input instanceof Headers || Array.isArray(input)) {
141
+ new Headers(input).forEach((value, name) => headers.append(name, value));
142
+ return headers;
143
+ }
144
+ if (hasHeaderForEach(input)) {
145
+ input.forEach((value, name) => headers.append(name, value));
146
+ return headers;
147
+ }
148
+ if (hasHeaderEntries(input)) {
149
+ for (const [name, value] of input.entries()) {
150
+ headers.append(name, value);
140
151
  }
141
- if (!key || !verifyJwtSignatureWithJwk(parsed, key)) {
142
- throw new Error("[@holo-js/auth-clerk] Clerk token signature verification failed.");
152
+ return headers;
153
+ }
154
+ if (hasHeaderGet(input)) {
155
+ appendKnownHeaders(headers, input);
156
+ return headers;
157
+ }
158
+ if (isPlainHeaderRecord(input)) {
159
+ for (const [name, value] of Object.entries(input)) {
160
+ if (typeof value === "string") {
161
+ headers.append(name, value);
162
+ continue;
163
+ }
164
+ if (Array.isArray(value)) {
165
+ const separator = name.toLowerCase() === "cookie" ? "; " : ",";
166
+ const joined = value.filter((entry) => typeof entry === "string").join(separator);
167
+ if (joined) {
168
+ headers.append(name, joined);
169
+ }
170
+ }
143
171
  }
144
172
  }
173
+ return headers;
174
+ }
175
+ function getRequestFromLikeInput(input) {
176
+ return input.request ?? input.web?.request ?? (input.req instanceof Request ? input.req : void 0);
177
+ }
178
+ function getRequestLikeHeaders(input) {
179
+ return input.headers ?? (typeof input.req === "object" && !(input.req instanceof Request) ? input.req.headers : void 0) ?? input.node?.req?.headers;
180
+ }
181
+ function getRequestLikeMethod(input) {
182
+ return input.method ?? (typeof input.req === "object" && !(input.req instanceof Request) ? input.req.method : void 0) ?? input.node?.req?.method ?? "GET";
183
+ }
184
+ function getRequestLikeUrl(input, headers) {
185
+ const url = (typeof input.url === "string" ? input.url : input.url?.toString()) ?? (typeof input.req === "object" && !(input.req instanceof Request) ? input.req.url : void 0) ?? input.node?.req?.url ?? input.path ?? "/";
186
+ try {
187
+ return new URL(url).toString();
188
+ } catch {
189
+ const protocol = headers.get("x-forwarded-proto") ?? "http";
190
+ const host = headers.get("x-forwarded-host") ?? headers.get("host") ?? "localhost";
191
+ return new URL(url, `${protocol}://${host}`).toString();
192
+ }
193
+ }
194
+ function normalizeClerkRequest(input) {
195
+ if (input instanceof Request) {
196
+ return input;
197
+ }
198
+ const request = getRequestFromLikeInput(input);
199
+ if (request) {
200
+ return request;
201
+ }
202
+ const headers = normalizeRequestHeaders(getRequestLikeHeaders(input));
203
+ return new Request(getRequestLikeUrl(input, headers), {
204
+ method: getRequestLikeMethod(input),
205
+ headers
206
+ });
207
+ }
208
+ function getBindings() {
209
+ const clerkBindings = getRuntimeState().bindings;
210
+ if (!clerkBindings?.identityStore) {
211
+ throwUnconfigured();
212
+ }
213
+ return {
214
+ providers: clerkBindings.providers ?? {},
215
+ identityStore: clerkBindings.identityStore
216
+ };
217
+ }
218
+ async function verifyClerkSessionToken(token, config, authorizedParties = []) {
219
+ const parsed = parseJwt(token);
220
+ const headerKid = typeof parsed.header.kid === "string" ? parsed.header.kid : void 0;
221
+ const jwksUrl = resolveClerkJwksUrl(config);
222
+ const resolveKey = async (refresh = false) => {
223
+ const keys = await fetchClerkJwks(jwksUrl, { refresh });
224
+ return headerKid ? keys.find((candidate) => candidate.kid === headerKid) : keys[0];
225
+ };
226
+ let key = await resolveKey();
227
+ if (!key || !verifyJwtSignatureWithJwk(parsed, key)) {
228
+ key = await resolveKey(true);
229
+ }
230
+ if (!key || !verifyJwtSignatureWithJwk(parsed, key)) {
231
+ throw new Error("[@holo-js/auth-clerk] Clerk token signature verification failed.");
232
+ }
145
233
  const exp = typeof parsed.payload.exp === "number" ? parsed.payload.exp : void 0;
146
- if (typeof exp === "number" && exp * 1e3 <= Date.now()) {
234
+ if (typeof exp !== "number") {
235
+ throw new Error("[@holo-js/auth-clerk] Clerk token did not include an expiration.");
236
+ }
237
+ if (exp * 1e3 <= Date.now()) {
147
238
  throw new Error("[@holo-js/auth-clerk] Clerk token has expired.");
148
239
  }
240
+ if (typeof parsed.payload.sub !== "string" || !parsed.payload.sub.trim()) {
241
+ throw new Error("[@holo-js/auth-clerk] Clerk token did not include a subject.");
242
+ }
243
+ if ((typeof parsed.payload.sid !== "string" || !parsed.payload.sid.trim()) && (typeof parsed.payload.session_id !== "string" || !parsed.payload.session_id.trim())) {
244
+ throw new Error("[@holo-js/auth-clerk] Clerk token did not include a session id.");
245
+ }
149
246
  const nbf = typeof parsed.payload.nbf === "number" ? parsed.payload.nbf : void 0;
150
247
  if (typeof nbf === "number" && nbf * 1e3 > Date.now()) {
151
248
  throw new Error("[@holo-js/auth-clerk] Clerk token is not valid yet.");
152
249
  }
153
250
  const azp = typeof parsed.payload.azp === "string" ? parsed.payload.azp.trim() : "";
154
- if (azp) {
155
- const allowedAuthorizedParties = authorizedParties.map((value) => value.trim()).filter(Boolean);
156
- if (allowedAuthorizedParties.length > 0 && !allowedAuthorizedParties.includes(azp)) {
157
- throw new Error(`[@holo-js/auth-clerk] Clerk token authorized party "${azp}" is not allowed.`);
158
- }
251
+ const allowedAuthorizedParties = authorizedParties.map((value) => value.trim()).filter(Boolean);
252
+ if (allowedAuthorizedParties.length > 0 && !azp) {
253
+ throw new Error("[@holo-js/auth-clerk] Clerk token did not include an authorized party.");
254
+ }
255
+ if (azp && allowedAuthorizedParties.length > 0 && !allowedAuthorizedParties.includes(azp)) {
256
+ throw new Error(`[@holo-js/auth-clerk] Clerk token authorized party "${azp}" is not allowed.`);
159
257
  }
160
258
  return parsed.payload;
161
259
  }
@@ -168,16 +266,24 @@ function normalizeClerkEmailAddress(value) {
168
266
  });
169
267
  }
170
268
  function normalizeClerkUserProfile(user) {
269
+ const emailAddresses = Array.isArray(user.emailAddresses) ? user.emailAddresses.map((entry) => normalizeClerkEmailAddress(entry)) : Array.isArray(user.email_addresses) ? user.email_addresses.map((entry) => normalizeClerkEmailAddress(entry)) : void 0;
270
+ const primaryEmailAddressId = typeof user.primaryEmailAddressId === "string" ? user.primaryEmailAddressId : typeof user.primary_email_address_id === "string" ? user.primary_email_address_id : void 0;
271
+ const email = typeof user.email === "string" && user.email.trim() ? user.email.trim() : (emailAddresses?.find((entry) => entry.id === primaryEmailAddressId) ?? emailAddresses?.[0])?.emailAddress.trim() ?? "";
272
+ const emailVerified = user.emailVerified === true || user.email_verified === true || (emailAddresses?.find((entry) => entry.id === primaryEmailAddressId) ?? emailAddresses?.[0])?.verificationStatus === "verified";
273
+ const firstName = typeof user.firstName === "string" ? user.firstName : typeof user.first_name === "string" ? user.first_name : void 0;
274
+ const lastName = typeof user.lastName === "string" ? user.lastName : typeof user.last_name === "string" ? user.last_name : void 0;
275
+ const explicitName = typeof user.name === "string" ? user.name.trim() : "";
276
+ const name = explicitName || [firstName?.trim(), lastName?.trim()].filter(Boolean).join(" ").trim();
171
277
  return Object.freeze({
172
278
  id: String(user.id ?? ""),
173
- email: typeof user.email === "string" ? user.email : void 0,
174
- emailVerified: user.emailVerified === true || user.email_verified === true,
175
- firstName: typeof user.firstName === "string" ? user.firstName : typeof user.first_name === "string" ? user.first_name : void 0,
176
- lastName: typeof user.lastName === "string" ? user.lastName : typeof user.last_name === "string" ? user.last_name : void 0,
177
- name: typeof user.name === "string" ? user.name : void 0,
279
+ email,
280
+ emailVerified,
281
+ firstName,
282
+ lastName,
283
+ name: name || email || String(user.id ?? ""),
178
284
  imageUrl: typeof user.imageUrl === "string" ? user.imageUrl : typeof user.image_url === "string" ? user.image_url : void 0,
179
- primaryEmailAddressId: typeof user.primaryEmailAddressId === "string" ? user.primaryEmailAddressId : typeof user.primary_email_address_id === "string" ? user.primary_email_address_id : void 0,
180
- emailAddresses: Array.isArray(user.emailAddresses) ? user.emailAddresses.map((entry) => normalizeClerkEmailAddress(entry)) : Array.isArray(user.email_addresses) ? user.email_addresses.map((entry) => normalizeClerkEmailAddress(entry)) : void 0,
285
+ primaryEmailAddressId,
286
+ emailAddresses,
181
287
  raw: user
182
288
  });
183
289
  }
@@ -203,7 +309,6 @@ function createDefaultProviderRuntime(providerName, config) {
203
309
  providerName,
204
310
  config.apiUrl ?? "",
205
311
  config.frontendApi ?? "",
206
- config.jwtKey ?? "",
207
312
  config.secretKey ?? "",
208
313
  [...config.authorizedParties ?? []].sort()
209
314
  ]);
@@ -239,7 +344,11 @@ function resolveConfiguredProviderName(provider) {
239
344
  if (provider?.trim()) {
240
345
  return provider.trim();
241
346
  }
242
- const configuredProviders = Object.keys(bindings.config.clerk);
347
+ const configuredDefaultProvider = typeof bindings.config.clerk.provider === "string" ? bindings.config.clerk.provider.trim() : "";
348
+ if (configuredDefaultProvider) {
349
+ return configuredDefaultProvider;
350
+ }
351
+ const configuredProviders = Object.entries(bindings.config.clerk).flatMap(([name, value]) => name !== "provider" && name !== "identityStore" && isNormalizedClerkProviderConfig(value) ? [name] : []);
243
352
  if (configuredProviders.length === 0) {
244
353
  return "default";
245
354
  }
@@ -251,20 +360,23 @@ function resolveConfiguredProviderName(provider) {
251
360
  }
252
361
  throw new Error("[@holo-js/auth-clerk] Clerk provider name is required when multiple auth.clerk entries exist.");
253
362
  }
363
+ function isNormalizedClerkProviderConfig(value) {
364
+ return value !== null && typeof value === "object" && "sessionCookie" in value && typeof value.sessionCookie === "string" && "authorizedParties" in value && Array.isArray(value.authorizedParties);
365
+ }
254
366
  function getConfiguredProviderConfig(provider) {
255
367
  const providerName = resolveConfiguredProviderName(provider);
256
368
  const authBindings = authRuntimeInternals.getRuntimeBindings();
257
369
  const configured = authBindings.config.clerk[providerName];
258
- if (!configured) {
370
+ if (!isNormalizedClerkProviderConfig(configured)) {
259
371
  throw new Error(`[@holo-js/auth-clerk] Clerk provider "${providerName}" is not configured in auth.clerk.`);
260
372
  }
261
373
  return {
262
374
  name: providerName,
263
375
  publishableKey: configured.publishableKey,
264
376
  secretKey: configured.secretKey,
265
- jwtKey: configured.jwtKey,
266
377
  apiUrl: configured.apiUrl,
267
378
  frontendApi: configured.frontendApi,
379
+ redirectUri: configured.redirectUri,
268
380
  sessionCookie: configured.sessionCookie,
269
381
  authorizedParties: configured.authorizedParties ?? [],
270
382
  guard: configured.guard,
@@ -275,6 +387,73 @@ function getProviderRuntime(provider) {
275
387
  const providerName = resolveConfiguredProviderName(provider);
276
388
  return getBindings().providers[providerName] ?? createDefaultProviderRuntime(providerName, getConfiguredProviderConfig(providerName));
277
389
  }
390
+ function requireClerkHostedConfig(config) {
391
+ const frontendApi = config.frontendApi?.trim();
392
+ const redirectUri = config.redirectUri?.trim();
393
+ if (!frontendApi) {
394
+ throw new Error("[@holo-js/auth-clerk] Clerk hosted auth requires frontendApi to be configured.");
395
+ }
396
+ if (!redirectUri) {
397
+ throw new Error("[@holo-js/auth-clerk] Clerk hosted auth requires redirectUri to be configured.");
398
+ }
399
+ return { frontendApi, redirectUri };
400
+ }
401
+ function resolveClerkAccountPortalUrl(frontendApi) {
402
+ const url = new URL(frontendApi);
403
+ if (url.hostname.endsWith(".clerk.accounts.dev")) {
404
+ url.hostname = `${url.hostname.slice(0, -".clerk.accounts.dev".length)}.accounts.dev`;
405
+ } else if (url.hostname.startsWith("clerk.")) {
406
+ url.hostname = `accounts.${url.hostname.slice("clerk.".length)}`;
407
+ }
408
+ url.pathname = "/";
409
+ url.search = "";
410
+ url.hash = "";
411
+ return url.toString();
412
+ }
413
+ function createAuthorizationUrl(config, page) {
414
+ const { frontendApi, redirectUri } = requireClerkHostedConfig(config);
415
+ const portalBaseUrl = resolveClerkAccountPortalUrl(frontendApi);
416
+ const url = new URL(page, portalBaseUrl);
417
+ url.searchParams.set("redirect_url", redirectUri);
418
+ return url.toString();
419
+ }
420
+ function createClerkReturnUrl(request, returnTo) {
421
+ const requestUrl = new URL(request.url);
422
+ const safeDefault = new URL("/", requestUrl).toString();
423
+ const trimmed = returnTo?.trim();
424
+ if (!trimmed || hasControlCharacter(trimmed)) {
425
+ return safeDefault;
426
+ }
427
+ try {
428
+ const parsed = new URL(trimmed, requestUrl);
429
+ const isRelativePath = trimmed.startsWith("/") && !trimmed.startsWith("//");
430
+ const isAbsoluteUrl = /^[a-z][a-z\d+.-]*:/iu.test(trimmed);
431
+ if (parsed.origin !== requestUrl.origin || !parsed.pathname.startsWith("/") || !isRelativePath && !isAbsoluteUrl) {
432
+ return safeDefault;
433
+ }
434
+ return parsed.toString();
435
+ } catch {
436
+ return safeDefault;
437
+ }
438
+ }
439
+ function hasControlCharacter(value) {
440
+ for (const character of value) {
441
+ const codePoint = character.codePointAt(0);
442
+ if (typeof codePoint === "number" && (codePoint < 32 || codePoint === 127)) {
443
+ return true;
444
+ }
445
+ }
446
+ return false;
447
+ }
448
+ function createClerkErrorResponse(error, code) {
449
+ return Response.json({
450
+ ok: false,
451
+ code,
452
+ message: getErrorMessage(error)
453
+ }, {
454
+ status: error instanceof ClerkAuthConflictError ? 409 : 422
455
+ });
456
+ }
278
457
  function resolveGuardAndProvider(provider) {
279
458
  const authBindings = authRuntimeInternals.getRuntimeBindings();
280
459
  const providerConfig = getConfiguredProviderConfig(provider);
@@ -313,6 +492,46 @@ function requireUserRecord(user, message) {
313
492
  }
314
493
  return user;
315
494
  }
495
+ function requireSerializedUser(user, message) {
496
+ if (typeof user.id === "string" || typeof user.id === "number") {
497
+ return user;
498
+ }
499
+ throw new Error(message);
500
+ }
501
+ function createClerkSessionPayload(authenticated, session) {
502
+ const user = requireSerializedUser(
503
+ authenticated.user,
504
+ "[@holo-js/auth-clerk] Clerk-authenticated local users must expose a serializable id."
505
+ );
506
+ return Object.freeze({
507
+ guard: authenticated.guard,
508
+ provider: authenticated.authProvider,
509
+ userId: user.id,
510
+ user,
511
+ clerk: Object.freeze({
512
+ provider: authenticated.provider,
513
+ sessionId: session.sessionId
514
+ })
515
+ });
516
+ }
517
+ function getClerkLogoutSession(payload, providerName) {
518
+ if (!payload || typeof payload !== "object" || !("clerk" in payload)) {
519
+ return null;
520
+ }
521
+ const clerk = payload.clerk;
522
+ if (!clerk || typeof clerk !== "object") {
523
+ return null;
524
+ }
525
+ const provider = clerk.provider;
526
+ const sessionId = clerk.sessionId;
527
+ if (provider !== providerName || typeof sessionId !== "string" || !sessionId.trim()) {
528
+ return null;
529
+ }
530
+ return Object.freeze({
531
+ provider,
532
+ sessionId
533
+ });
534
+ }
316
535
  function serializeLocalUser(adapter, user, providerName) {
317
536
  const id = adapter.getId(user);
318
537
  const serialized = adapter.serialize ? adapter.serialize(user) : { ...user };
@@ -325,6 +544,11 @@ function serializeLocalUser(adapter, user, providerName) {
325
544
  enumerable: false,
326
545
  configurable: true
327
546
  });
547
+ Object.defineProperty(result, "can", {
548
+ value: () => false,
549
+ enumerable: false,
550
+ configurable: true
551
+ });
328
552
  return Object.freeze(result);
329
553
  }
330
554
  function resolvePrimaryEmail(profile) {
@@ -351,7 +575,7 @@ function resolveDisplayName(profile) {
351
575
  return profile.name.trim();
352
576
  }
353
577
  const fullName = [profile.firstName?.trim(), profile.lastName?.trim()].filter(Boolean).join(" ").trim();
354
- return fullName || void 0;
578
+ return fullName || profile.email || profile.id;
355
579
  }
356
580
  function resolveEmailForCreation(profile) {
357
581
  const primary = resolvePrimaryEmail(profile).email;
@@ -360,6 +584,34 @@ function resolveEmailForCreation(profile) {
360
584
  }
361
585
  return `${profile.id}@clerk.hosted.local`;
362
586
  }
587
+ function toAdapterInput(input) {
588
+ return Object.freeze(Object.fromEntries(
589
+ Object.entries(input).filter((entry) => typeof entry[1] !== "undefined")
590
+ ));
591
+ }
592
+ function withRequiredUserFields(input, profile) {
593
+ return Object.freeze({
594
+ ...input,
595
+ email: typeof input.email === "undefined" ? resolveEmailForCreation(profile) : input.email,
596
+ name: typeof input.name === "undefined" ? resolveDisplayName(profile) : input.name
597
+ });
598
+ }
599
+ function resolveCreateUserInput(profile, mapper) {
600
+ return toAdapterInput(withRequiredUserFields({
601
+ password: null,
602
+ avatar: profile.imageUrl ?? null,
603
+ email_verified_at: profile.emailVerified ? /* @__PURE__ */ new Date() : null,
604
+ ...mapper?.(profile) ?? {}
605
+ }, profile));
606
+ }
607
+ function resolveUpdateUserInput(profile, mapper) {
608
+ return toAdapterInput(withRequiredUserFields({
609
+ email: profile.email.trim() || void 0,
610
+ avatar: profile.imageUrl,
611
+ email_verified_at: profile.emailVerified ? /* @__PURE__ */ new Date() : void 0,
612
+ ...mapper?.(profile) ?? {}
613
+ }, profile));
614
+ }
363
615
  function normalizeHostedProfile(profile) {
364
616
  const resolvedEmail = resolvePrimaryEmail(profile);
365
617
  return Object.freeze({
@@ -383,21 +635,20 @@ async function findUserByEmail(adapter, email) {
383
635
  return user ? requireUserRecord(user, "[@holo-js/auth-clerk] Auth provider lookups must return object users.") : null;
384
636
  }
385
637
  async function updateLocalUser(adapter, user, input) {
386
- const nextInput = {
387
- name: input.name,
388
- email: input.email,
389
- avatar: typeof input.avatar === "undefined" ? void 0 : input.avatar,
390
- email_verified_at: input.emailVerified === true ? /* @__PURE__ */ new Date() : void 0
391
- };
392
638
  const current = user;
393
- const changed = typeof nextInput.name !== "undefined" && nextInput.name !== current.name || typeof nextInput.email !== "undefined" && nextInput.email !== current.email || typeof nextInput.avatar !== "undefined" && nextInput.avatar !== (current.avatar ?? void 0) || input.emailVerified === true && !current.email_verified_at;
639
+ const changed = Object.entries(input).some(([key, value]) => {
640
+ if (key === "email_verified_at") {
641
+ return value instanceof Date && !current.email_verified_at;
642
+ }
643
+ return !Object.is(value, user[key]);
644
+ });
394
645
  if (!changed) {
395
646
  return { user, changed: false };
396
647
  }
397
648
  if (adapter.update) {
398
649
  return {
399
650
  user: requireUserRecord(
400
- await adapter.update(user, nextInput),
651
+ await adapter.update(user, input),
401
652
  "[@holo-js/auth-clerk] Auth provider updates must return object users."
402
653
  ),
403
654
  changed: true
@@ -407,7 +658,7 @@ async function updateLocalUser(adapter, user, input) {
407
658
  "[@holo-js/auth-clerk] Auth provider adapters must implement update() to persist profile changes."
408
659
  );
409
660
  }
410
- async function ensureNoUnexpectedEmailCollision(adapter, authProvider, profile, currentUserId) {
661
+ async function ensureNoUnexpectedEmailCollision(adapter, providerName, profile, currentUserId) {
411
662
  const resolvedEmail = resolvePrimaryEmail(profile).email;
412
663
  if (!resolvedEmail) {
413
664
  return;
@@ -422,28 +673,13 @@ async function ensureNoUnexpectedEmailCollision(adapter, authProvider, profile,
422
673
  "[@holo-js/auth-clerk] Matched local users must expose a serializable id."
423
674
  ) !== currentUserId) {
424
675
  throw new ClerkAuthConflictError({
425
- provider: "clerk",
676
+ provider: providerName,
426
677
  clerkUserId: profile.id,
427
678
  email: resolvedEmail,
428
679
  message: `[@holo-js/auth-clerk] Clerk email "${resolvedEmail}" collides with a different local user.`
429
680
  });
430
681
  }
431
682
  }
432
- async function assertUserLinkAvailable(providerName, authProvider, adapter, user, clerkUserId) {
433
- const existing = await getBindings().identityStore.findByUserId(
434
- providerName,
435
- authProvider,
436
- requireUserId(adapter, user, "[@holo-js/auth-clerk] Linked users must expose a serializable id.")
437
- );
438
- if (existing && existing.providerUserId !== clerkUserId) {
439
- throw new ClerkAuthConflictError({
440
- provider: providerName,
441
- clerkUserId,
442
- email: existing.email,
443
- message: `[@holo-js/auth-clerk] Local user is already linked to Clerk identity "${existing.providerUserId}".`
444
- });
445
- }
446
- }
447
683
  function isEmailVerificationRequired() {
448
684
  return authRuntimeInternals.getRuntimeBindings().config.emailVerification.required === true;
449
685
  }
@@ -463,6 +699,47 @@ function createIdentityRecord(input) {
463
699
  updatedAt: now
464
700
  });
465
701
  }
702
+ async function withIdentitySyncLock(key, callback) {
703
+ const previous = clerkIdentitySyncLocks.get(key) ?? Promise.resolve();
704
+ let release = () => {
705
+ };
706
+ const current = previous.then(() => new Promise((resolve) => {
707
+ release = resolve;
708
+ }));
709
+ clerkIdentitySyncLocks.set(key, current);
710
+ await previous;
711
+ try {
712
+ return await callback();
713
+ } finally {
714
+ release();
715
+ if (clerkIdentitySyncLocks.get(key) === current) {
716
+ clerkIdentitySyncLocks.delete(key);
717
+ }
718
+ }
719
+ }
720
+ async function claimNewIdentity(identityStore, identity) {
721
+ if (identityStore.claim) {
722
+ return await identityStore.claim(identity);
723
+ }
724
+ await identityStore.save(identity);
725
+ return identity;
726
+ }
727
+ function sameUserId(left, right) {
728
+ return String(left) === String(right);
729
+ }
730
+ async function resolveClaimedIdentityUser(adapter, identity, fallback) {
731
+ if (sameUserId(identity.userId, fallback.userId)) {
732
+ return fallback.user;
733
+ }
734
+ const claimedUser = requireUserRecord(
735
+ await adapter.findById(identity.userId),
736
+ "[@holo-js/auth-clerk] Claimed Clerk identities must reference an existing local user."
737
+ );
738
+ if (fallback.deleteOnMismatch && adapter.delete) {
739
+ await adapter.delete(fallback.userId);
740
+ }
741
+ return claimedUser;
742
+ }
466
743
  function getSessionTokenFromRequest(request, sessionCookie) {
467
744
  const authorization = request.headers.get("authorization")?.trim();
468
745
  if (authorization) {
@@ -474,6 +751,53 @@ function getSessionTokenFromRequest(request, sessionCookie) {
474
751
  const cookies = parseCookieHeader(request.headers.get("cookie"));
475
752
  return cookies[sessionCookie] ?? null;
476
753
  }
754
+ function hasClerkCallbackState(request) {
755
+ const url = new URL(request.url);
756
+ return [...url.searchParams.keys()].some((key) => key.startsWith("__clerk"));
757
+ }
758
+ function hasClerkHandshakeAttempt(request) {
759
+ const url = new URL(request.url);
760
+ if (url.searchParams.has("__clerk_handshake") || url.searchParams.has("__clerk_handshake_nonce") || url.searchParams.has("__clerk_synced")) {
761
+ return true;
762
+ }
763
+ const cookies = parseCookieHeader(request.headers.get("cookie"));
764
+ const redirectCount = cookies.__clerk_redirect_count;
765
+ return typeof redirectCount === "string" && redirectCount !== "" && redirectCount !== "0";
766
+ }
767
+ function isConfiguredClerkRedirectRequest(request, config) {
768
+ const redirectUri = config.redirectUri?.trim();
769
+ if (!redirectUri) {
770
+ return hasClerkCallbackState(request);
771
+ }
772
+ try {
773
+ const requestUrl = new URL(request.url);
774
+ const configuredUrl = new URL(redirectUri, requestUrl.origin);
775
+ return requestUrl.origin === configuredUrl.origin && requestUrl.pathname === configuredUrl.pathname;
776
+ } catch {
777
+ return false;
778
+ }
779
+ }
780
+ function splitSetCookieHeader(value) {
781
+ return value.split(/,(?=\s*[^;,=\s]+=)/g).map((cookie) => cookie.trim()).filter(Boolean);
782
+ }
783
+ function getSetCookieHeaders(headers) {
784
+ const withGetSetCookie = headers;
785
+ const explicit = withGetSetCookie.getSetCookie?.();
786
+ if (explicit?.length) {
787
+ return explicit;
788
+ }
789
+ const combined = headers.get("set-cookie");
790
+ return combined ? splitSetCookieHeader(combined) : [];
791
+ }
792
+ async function appendClerkResponseCookies(headers) {
793
+ const append = authRuntimeInternals.getRuntimeBindings().context.appendResponseCookie;
794
+ if (!append) {
795
+ return;
796
+ }
797
+ for (const cookie of getSetCookieHeaders(headers)) {
798
+ await append(cookie);
799
+ }
800
+ }
477
801
  function getHoloSessionIdFromRequest(request) {
478
802
  const cookieHeader = authRuntimeInternals.getRuntimeBindings().session.sessionCookie("");
479
803
  const separator = cookieHeader.indexOf("=");
@@ -496,9 +820,16 @@ async function reuseExistingHoloSession(request, authenticated) {
496
820
  if (!payload || payload.guard !== authenticated.guard || payload.provider !== authenticated.authProvider || String(payload.userId) !== String(authenticated.user.id)) {
497
821
  return null;
498
822
  }
823
+ const source = payload;
824
+ if (source.clerk && typeof source.clerk === "object") {
825
+ if (typeof source.clerk.provider !== "string" || source.clerk.provider !== authenticated.provider) {
826
+ return null;
827
+ }
828
+ }
499
829
  bindings.context.setCachedUser(authenticated.guard, authenticated.user);
500
830
  return Object.freeze({
501
831
  guard: authenticated.guard,
832
+ provider: source.clerk && typeof source.clerk === "object" ? "clerk" : payload.provider,
502
833
  user: authenticated.user,
503
834
  sessionId,
504
835
  cookies: Object.freeze([])
@@ -516,7 +847,71 @@ async function verifySession(token, provider) {
516
847
  config: providerConfig
517
848
  });
518
849
  }
519
- async function verifyRequest(request, provider) {
850
+ function resolveRequestAuthorizedParties(config, request) {
851
+ return Object.freeze([
852
+ .../* @__PURE__ */ new Set([
853
+ ...config.authorizedParties ?? [],
854
+ new URL(request.url).origin
855
+ ])
856
+ ]);
857
+ }
858
+ async function verifyDefaultSessionToken(token, providerConfig, authorizedParties) {
859
+ const config = {
860
+ ...providerConfig,
861
+ authorizedParties
862
+ };
863
+ const defaultRuntime = createDefaultProviderRuntime(providerConfig.name, config);
864
+ if (!defaultRuntime.verifySession) {
865
+ throw new Error(`[@holo-js/auth-clerk] Clerk provider runtime "${providerConfig.name}" does not implement verifySession().`);
866
+ }
867
+ return defaultRuntime.verifySession({
868
+ provider: providerConfig.name,
869
+ token,
870
+ config
871
+ });
872
+ }
873
+ async function verifyRequestWithClerkBackendSdk(request, providerConfig) {
874
+ const publishableKey = providerConfig.publishableKey?.trim();
875
+ const secretKey = providerConfig.secretKey?.trim();
876
+ if (!publishableKey || !secretKey) {
877
+ return null;
878
+ }
879
+ const { createClerkClient } = await import("@clerk/backend");
880
+ const client = createClerkClient({
881
+ apiUrl: providerConfig.apiUrl,
882
+ publishableKey,
883
+ secretKey
884
+ });
885
+ const authenticateOptions = {
886
+ apiUrl: providerConfig.apiUrl,
887
+ authorizedParties: [...resolveRequestAuthorizedParties(providerConfig, request)],
888
+ publishableKey,
889
+ secretKey
890
+ };
891
+ const requestState = await client.authenticateRequest(request, authenticateOptions);
892
+ await appendClerkResponseCookies(requestState.headers);
893
+ if (requestState.status === "handshake") {
894
+ const redirectUrl = requestState.headers.get("location");
895
+ if (redirectUrl && !hasClerkHandshakeAttempt(request)) {
896
+ await authRuntimeInternals.redirectResponse(
897
+ authRuntimeInternals.getRuntimeBindings(),
898
+ new URL(redirectUrl, request.url).toString(),
899
+ 307
900
+ );
901
+ }
902
+ return null;
903
+ }
904
+ if (requestState.status !== "signed-in") {
905
+ return null;
906
+ }
907
+ return verifyDefaultSessionToken(
908
+ requestState.token,
909
+ providerConfig,
910
+ resolveRequestAuthorizedParties(providerConfig, request)
911
+ );
912
+ }
913
+ async function verifyRequest(input, provider) {
914
+ const request = normalizeClerkRequest(input);
520
915
  const providerConfig = getConfiguredProviderConfig(provider);
521
916
  const configuredRuntime = getBindings().providers[providerConfig.name];
522
917
  const runtime = configuredRuntime ?? getProviderRuntime(providerConfig.name);
@@ -528,6 +923,12 @@ async function verifyRequest(request, provider) {
528
923
  });
529
924
  }
530
925
  const token = getSessionTokenFromRequest(request, providerConfig.sessionCookie);
926
+ if (!configuredRuntime && (hasClerkCallbackState(request) || isConfiguredClerkRedirectRequest(request, providerConfig))) {
927
+ const clerkSession = await verifyRequestWithClerkBackendSdk(request, providerConfig);
928
+ if (clerkSession) {
929
+ return clerkSession;
930
+ }
931
+ }
531
932
  if (!token) {
532
933
  return null;
533
934
  }
@@ -541,34 +942,9 @@ async function verifyRequest(request, provider) {
541
942
  config: providerConfig
542
943
  });
543
944
  }
544
- const requestOrigin = new URL(request.url).origin;
545
- const defaultRuntime = createDefaultProviderRuntime(providerConfig.name, {
546
- ...providerConfig,
547
- authorizedParties: Object.freeze([
548
- .../* @__PURE__ */ new Set([
549
- ...providerConfig.authorizedParties ?? [],
550
- requestOrigin
551
- ])
552
- ])
553
- });
554
- if (!defaultRuntime.verifySession) {
555
- throw new Error(`[@holo-js/auth-clerk] Clerk provider runtime "${providerConfig.name}" does not implement verifySession().`);
556
- }
557
- return defaultRuntime.verifySession({
558
- provider: providerConfig.name,
559
- token,
560
- config: {
561
- ...providerConfig,
562
- authorizedParties: Object.freeze([
563
- .../* @__PURE__ */ new Set([
564
- ...providerConfig.authorizedParties ?? [],
565
- requestOrigin
566
- ])
567
- ])
568
- }
569
- });
945
+ return verifyDefaultSessionToken(token, providerConfig, resolveRequestAuthorizedParties(providerConfig, request));
570
946
  }
571
- async function syncIdentity(session, provider) {
947
+ async function syncIdentity(session, provider, options = {}) {
572
948
  const providerConfig = getConfiguredProviderConfig(provider);
573
949
  const providerName = providerConfig.name;
574
950
  const profile = session.user;
@@ -579,155 +955,129 @@ async function syncIdentity(session, provider) {
579
955
  if (verificationRequired && !verifiedEmail) {
580
956
  throw new Error(`[@holo-js/auth-clerk] Clerk identity "${profile.id}" must provide a verified email address.`);
581
957
  }
582
- const identityStore = getBindings().identityStore;
583
- const existingIdentity = await identityStore.findByProviderUserId(providerName, profile.id);
584
- if (existingIdentity) {
585
- const existingLinkedUser = await adapter.findById(existingIdentity.userId);
586
- let linkedUser = existingLinkedUser ? requireUserRecord(existingLinkedUser, "[@holo-js/auth-clerk] Auth provider lookups must return object users.") : null;
587
- if (!linkedUser) {
588
- linkedUser = verifiedEmail ? await findUserByEmail(adapter, verifiedEmail) : null;
589
- if (linkedUser) {
590
- await assertUserLinkAvailable(providerName, authProvider, adapter, linkedUser, profile.id);
591
- }
958
+ return await withIdentitySyncLock(`${providerName}:${profile.id}`, async () => {
959
+ const identityStore = getBindings().identityStore;
960
+ const existingIdentity = await identityStore.findByProviderUserId(providerName, profile.id);
961
+ if (existingIdentity) {
962
+ const existingLinkedUser = await adapter.findById(existingIdentity.userId);
963
+ let linkedUser = existingLinkedUser ? requireUserRecord(existingLinkedUser, "[@holo-js/auth-clerk] Auth provider lookups must return object users.") : null;
592
964
  if (!linkedUser) {
593
- linkedUser = requireUserRecord(await adapter.create({
594
- name: resolveDisplayName(profile),
595
- email: resolveEmailForCreation(profile),
596
- password: null,
597
- avatar: profile.imageUrl ?? null,
598
- email_verified_at: resolvedEmail.emailVerified ? /* @__PURE__ */ new Date() : null
599
- }), "[@holo-js/auth-clerk] Auth provider create() must return an object user.");
965
+ const emailMatchedUser = verifiedEmail ? await findUserByEmail(adapter, verifiedEmail) : null;
966
+ if (emailMatchedUser) {
967
+ throw new ClerkAuthConflictError({
968
+ provider: providerName,
969
+ clerkUserId: profile.id,
970
+ email: verifiedEmail,
971
+ message: `[@holo-js/auth-clerk] Clerk email "${verifiedEmail}" matches an existing local user and must be linked explicitly.`
972
+ });
973
+ }
974
+ linkedUser = requireUserRecord(
975
+ await adapter.create(resolveCreateUserInput(profile, options.user)),
976
+ "[@holo-js/auth-clerk] Auth provider create() must return an object user."
977
+ );
978
+ const relinked = await updateLocalUser(adapter, linkedUser, resolveUpdateUserInput(profile, options.user));
979
+ const relinkedUser = relinked.user;
980
+ const identity3 = createIdentityRecord({
981
+ provider: providerName,
982
+ guard,
983
+ authProvider,
984
+ userId: requireUserId(
985
+ adapter,
986
+ relinkedUser,
987
+ "[@holo-js/auth-clerk] Relinked local users must expose a serializable id."
988
+ ),
989
+ profile,
990
+ previous: existingIdentity
991
+ });
992
+ await identityStore.save(identity3);
993
+ return Object.freeze({
994
+ provider: providerName,
995
+ guard,
996
+ authProvider,
997
+ status: "relinked",
998
+ user: serializeLocalUser(adapter, relinkedUser, authProvider),
999
+ identity: identity3,
1000
+ session
1001
+ });
600
1002
  }
601
- const relinked = await updateLocalUser(adapter, linkedUser, {
602
- name: resolveDisplayName(profile),
603
- email: resolvedEmail.email,
604
- avatar: profile.imageUrl,
605
- emailVerified: resolvedEmail.emailVerified
606
- });
607
- const relinkedUser = relinked.user;
608
- const identity3 = createIdentityRecord({
1003
+ await ensureNoUnexpectedEmailCollision(
1004
+ adapter,
1005
+ providerName,
1006
+ profile,
1007
+ requireUserId(
1008
+ adapter,
1009
+ linkedUser,
1010
+ "[@holo-js/auth-clerk] Linked local users must expose a serializable id."
1011
+ )
1012
+ );
1013
+ const updated = await updateLocalUser(adapter, linkedUser, resolveUpdateUserInput(profile, options.user));
1014
+ const identity2 = createIdentityRecord({
609
1015
  provider: providerName,
610
1016
  guard,
611
1017
  authProvider,
612
1018
  userId: requireUserId(
613
1019
  adapter,
614
- relinkedUser,
615
- "[@holo-js/auth-clerk] Relinked local users must expose a serializable id."
1020
+ updated.user,
1021
+ "[@holo-js/auth-clerk] Updated local users must expose a serializable id."
616
1022
  ),
617
1023
  profile,
618
1024
  previous: existingIdentity
619
1025
  });
620
- await identityStore.save(identity3);
1026
+ await identityStore.save(identity2);
621
1027
  return Object.freeze({
622
1028
  provider: providerName,
623
1029
  guard,
624
1030
  authProvider,
625
- status: "relinked",
626
- user: serializeLocalUser(adapter, relinkedUser, authProvider),
627
- identity: identity3,
1031
+ status: updated.changed ? "updated" : "linked",
1032
+ user: serializeLocalUser(adapter, updated.user, authProvider),
1033
+ identity: identity2,
628
1034
  session
629
1035
  });
630
1036
  }
631
- await ensureNoUnexpectedEmailCollision(
632
- adapter,
633
- authProvider,
634
- profile,
635
- requireUserId(
636
- adapter,
637
- linkedUser,
638
- "[@holo-js/auth-clerk] Linked local users must expose a serializable id."
639
- )
1037
+ let localUser = verifiedEmail ? await findUserByEmail(adapter, verifiedEmail) : null;
1038
+ if (localUser) {
1039
+ throw new ClerkAuthConflictError({
1040
+ provider: providerName,
1041
+ clerkUserId: profile.id,
1042
+ email: verifiedEmail,
1043
+ message: `[@holo-js/auth-clerk] Clerk email "${verifiedEmail}" matches an existing local user and must be linked explicitly.`
1044
+ });
1045
+ }
1046
+ localUser = requireUserRecord(
1047
+ await adapter.create(resolveCreateUserInput(profile, options.user)),
1048
+ "[@holo-js/auth-clerk] Auth provider create() must return an object user."
640
1049
  );
641
- const updated = await updateLocalUser(adapter, linkedUser, {
642
- name: resolveDisplayName(profile),
643
- email: resolvedEmail.email,
644
- avatar: profile.imageUrl,
645
- emailVerified: resolvedEmail.emailVerified
646
- });
647
- const identity2 = createIdentityRecord({
1050
+ const identity = createIdentityRecord({
648
1051
  provider: providerName,
649
1052
  guard,
650
1053
  authProvider,
651
1054
  userId: requireUserId(
652
1055
  adapter,
653
- updated.user,
654
- "[@holo-js/auth-clerk] Updated local users must expose a serializable id."
655
- ),
656
- profile,
657
- previous: existingIdentity
658
- });
659
- await identityStore.save(identity2);
660
- return Object.freeze({
661
- provider: providerName,
662
- guard,
663
- authProvider,
664
- status: updated.changed ? "updated" : "linked",
665
- user: serializeLocalUser(adapter, updated.user, authProvider),
666
- identity: identity2,
667
- session
668
- });
669
- }
670
- let localUser = verifiedEmail ? await findUserByEmail(adapter, verifiedEmail) : null;
671
- if (localUser) {
672
- await assertUserLinkAvailable(providerName, authProvider, adapter, localUser, profile.id);
673
- const linked = await updateLocalUser(adapter, localUser, {
674
- name: resolveDisplayName(profile),
675
- email: resolvedEmail.email,
676
- avatar: profile.imageUrl,
677
- emailVerified: resolvedEmail.emailVerified
678
- });
679
- const identity2 = createIdentityRecord({
680
- provider: providerName,
681
- guard,
682
- authProvider,
683
- userId: requireUserId(
684
- adapter,
685
- linked.user,
686
- "[@holo-js/auth-clerk] Linked local users must expose a serializable id."
1056
+ localUser,
1057
+ "[@holo-js/auth-clerk] Created local users must expose a serializable id."
687
1058
  ),
688
1059
  profile
689
1060
  });
690
- await identityStore.save(identity2);
1061
+ const claimedIdentity = await claimNewIdentity(identityStore, identity);
1062
+ const claimedUser = await resolveClaimedIdentityUser(adapter, claimedIdentity, {
1063
+ user: localUser,
1064
+ userId: identity.userId,
1065
+ deleteOnMismatch: true
1066
+ });
1067
+ const claimedStatus = sameUserId(claimedIdentity.userId, identity.userId) ? "created" : "linked";
691
1068
  return Object.freeze({
692
1069
  provider: providerName,
693
1070
  guard,
694
1071
  authProvider,
695
- status: "linked",
696
- user: serializeLocalUser(adapter, linked.user, authProvider),
697
- identity: identity2,
1072
+ status: claimedStatus,
1073
+ user: serializeLocalUser(adapter, claimedUser, authProvider),
1074
+ identity: claimedIdentity,
698
1075
  session
699
1076
  });
700
- }
701
- localUser = requireUserRecord(await adapter.create({
702
- name: resolveDisplayName(profile),
703
- email: resolveEmailForCreation(profile),
704
- password: null,
705
- avatar: profile.imageUrl ?? null,
706
- email_verified_at: resolvedEmail.emailVerified ? /* @__PURE__ */ new Date() : null
707
- }), "[@holo-js/auth-clerk] Auth provider create() must return an object user.");
708
- const identity = createIdentityRecord({
709
- provider: providerName,
710
- guard,
711
- authProvider,
712
- userId: requireUserId(
713
- adapter,
714
- localUser,
715
- "[@holo-js/auth-clerk] Created local users must expose a serializable id."
716
- ),
717
- profile
718
- });
719
- await identityStore.save(identity);
720
- return Object.freeze({
721
- provider: providerName,
722
- guard,
723
- authProvider,
724
- status: "created",
725
- user: serializeLocalUser(adapter, localUser, authProvider),
726
- identity,
727
- session
728
1077
  });
729
1078
  }
730
- async function authenticate(request, provider) {
1079
+ async function authenticate(input, provider) {
1080
+ const request = normalizeClerkRequest(input);
731
1081
  const session = await verifyRequest(request, provider);
732
1082
  if (!session) {
733
1083
  return null;
@@ -735,28 +1085,177 @@ async function authenticate(request, provider) {
735
1085
  const authenticated = await syncIdentity(session, provider);
736
1086
  const authSession = await reuseExistingHoloSession(request, authenticated) ?? await authRuntimeInternals.establishSessionForUser(authenticated.user, {
737
1087
  guard: authenticated.guard,
738
- provider: authenticated.authProvider
1088
+ provider: authenticated.authProvider,
1089
+ payload: createClerkSessionPayload(authenticated, session)
739
1090
  });
740
1091
  return Object.freeze({
741
1092
  ...authenticated,
742
1093
  authSession
743
1094
  });
744
1095
  }
1096
+ function getErrorMessage(error) {
1097
+ return error instanceof Error ? error.message : "Clerk authentication failed.";
1098
+ }
1099
+ function createHostedAuthSuccess(data) {
1100
+ return Object.freeze({
1101
+ data,
1102
+ error: null
1103
+ });
1104
+ }
1105
+ function createHostedAuthFailure(code, message, status) {
1106
+ return Object.freeze({
1107
+ data: null,
1108
+ error: Object.freeze({
1109
+ code,
1110
+ message,
1111
+ status,
1112
+ fields: Object.freeze({
1113
+ _root: Object.freeze([message])
1114
+ })
1115
+ })
1116
+ });
1117
+ }
1118
+ async function loginWithClerk(_input, options = {}) {
1119
+ try {
1120
+ const providerConfig = getConfiguredProviderConfig(options.provider);
1121
+ return Response.redirect(createAuthorizationUrl(providerConfig, "sign-in"), 302);
1122
+ } catch (error) {
1123
+ return createClerkErrorResponse(error, "clerk_login_failed");
1124
+ }
1125
+ }
1126
+ async function registerWithClerk(_input, options = {}) {
1127
+ try {
1128
+ const providerConfig = getConfiguredProviderConfig(options.provider);
1129
+ return Response.redirect(createAuthorizationUrl(providerConfig, "sign-up"), 302);
1130
+ } catch (error) {
1131
+ return createClerkErrorResponse(error, "clerk_register_failed");
1132
+ }
1133
+ }
1134
+ async function readCurrentClerkLogoutSession(guard, providerName) {
1135
+ const bindings = authRuntimeInternals.getRuntimeBindings();
1136
+ const sessionId = bindings.context.getSessionId(guard);
1137
+ if (!sessionId) {
1138
+ return null;
1139
+ }
1140
+ const record = await bindings.session.read(sessionId);
1141
+ return getClerkLogoutSession(authRuntimeInternals.readSessionPayload(record, guard), providerName);
1142
+ }
1143
+ async function revokeClerkSession(config, sessionId) {
1144
+ const secretKey = config.secretKey?.trim();
1145
+ if (!secretKey) {
1146
+ throw new Error("[@holo-js/auth-clerk] Clerk logout requires secretKey to be configured.");
1147
+ }
1148
+ const apiBase = config.apiUrl?.trim() || CLERK_API_BASE_URL;
1149
+ const response = await fetch(`${apiBase.replace(/\/$/, "")}/v1/sessions/${encodeURIComponent(sessionId)}/revoke`, {
1150
+ method: "POST",
1151
+ headers: {
1152
+ accept: "application/json",
1153
+ authorization: `Bearer ${secretKey}`
1154
+ }
1155
+ });
1156
+ if (!response.ok) {
1157
+ throw new Error(`[@holo-js/auth-clerk] Failed to revoke Clerk session "${sessionId}".`);
1158
+ }
1159
+ }
1160
+ async function logoutWithClerk(input, options = {}) {
1161
+ const request = normalizeClerkRequest(input);
1162
+ try {
1163
+ const providerConfig = getConfiguredProviderConfig(options.provider);
1164
+ const providerName = providerConfig.name;
1165
+ const { guard } = resolveGuardAndProvider(providerName);
1166
+ const requestSessionId = getHoloSessionIdFromRequest(request);
1167
+ if (requestSessionId) {
1168
+ authRuntimeInternals.getRuntimeBindings().context.setSessionId(guard, requestSessionId);
1169
+ }
1170
+ const clerkSession = await readCurrentClerkLogoutSession(guard, providerName);
1171
+ if (!clerkSession) {
1172
+ return createHostedAuthFailure(
1173
+ "clerk_session_missing",
1174
+ "The current Holo session was not created by Clerk.",
1175
+ 422
1176
+ );
1177
+ }
1178
+ await revokeClerkSession(providerConfig, clerkSession.sessionId);
1179
+ const local = await getAuthRuntime().guard(guard).logout();
1180
+ return createHostedAuthSuccess(Object.freeze({
1181
+ url: createClerkReturnUrl(request, options.returnTo),
1182
+ local
1183
+ }));
1184
+ } catch {
1185
+ return createHostedAuthFailure("clerk_logout_failed", "Unable to complete Clerk logout.", 500);
1186
+ }
1187
+ }
1188
+ async function completeClerkAuth(input, options = {}) {
1189
+ try {
1190
+ const request = normalizeClerkRequest(input);
1191
+ const url = new URL(request.url);
1192
+ const callbackError = url.searchParams.get("error")?.trim();
1193
+ if (callbackError) {
1194
+ return createHostedAuthFailure(
1195
+ callbackError,
1196
+ url.searchParams.get("error_description")?.trim() || "Clerk authentication failed.",
1197
+ 422
1198
+ );
1199
+ }
1200
+ const providerConfig = getConfiguredProviderConfig(options.provider);
1201
+ const session = await verifyRequest(request, providerConfig.name);
1202
+ if (!session) {
1203
+ return createHostedAuthFailure(
1204
+ "clerk_session_required",
1205
+ "Clerk callback did not include an active session.",
1206
+ 422
1207
+ );
1208
+ }
1209
+ const authenticated = await syncIdentity(session, providerConfig.name, {
1210
+ user: options.user
1211
+ });
1212
+ const authSession = await authRuntimeInternals.establishSessionForUser(authenticated.user, {
1213
+ guard: authenticated.guard,
1214
+ provider: authenticated.authProvider,
1215
+ payload: createClerkSessionPayload(authenticated, session)
1216
+ });
1217
+ return createHostedAuthSuccess(Object.freeze({
1218
+ provider: authenticated.provider,
1219
+ guard: authenticated.guard,
1220
+ authProvider: authenticated.authProvider,
1221
+ status: authenticated.status,
1222
+ user: authenticated.user,
1223
+ identity: authenticated.identity,
1224
+ session,
1225
+ authSession
1226
+ }));
1227
+ } catch (error) {
1228
+ if (authRuntimeInternals.isResponseInterrupt(error)) {
1229
+ throw error;
1230
+ }
1231
+ return createHostedAuthFailure(
1232
+ error instanceof ClerkAuthConflictError ? error.code : "clerk_auth_failed",
1233
+ getErrorMessage(error),
1234
+ error instanceof ClerkAuthConflictError ? 409 : 422
1235
+ );
1236
+ }
1237
+ }
745
1238
  function configureClerkAuthRuntime(bindings) {
1239
+ const runtimeState = getRuntimeState();
746
1240
  if (!bindings) {
747
- clerkBindings = void 0;
1241
+ runtimeState.bindings = void 0;
748
1242
  return;
749
1243
  }
750
- clerkBindings = Object.freeze({
751
- providers: bindings.providers ?? clerkBindings?.providers,
752
- identityStore: bindings.identityStore ?? clerkBindings?.identityStore
1244
+ runtimeState.bindings = Object.freeze({
1245
+ providers: bindings.providers ?? runtimeState.bindings?.providers,
1246
+ identityStore: bindings.identityStore ?? runtimeState.bindings?.identityStore
753
1247
  });
754
1248
  }
755
1249
  function resetClerkAuthRuntime() {
756
- clerkBindings = void 0;
1250
+ getRuntimeState().bindings = void 0;
1251
+ clerkDefaultProviderRuntimeCache.clear();
757
1252
  }
758
1253
  var clerkAuth = Object.freeze({
759
1254
  authenticate,
1255
+ completeClerkAuth,
1256
+ loginWithClerk,
1257
+ logoutWithClerk,
1258
+ registerWithClerk,
760
1259
  syncIdentity,
761
1260
  verifyRequest,
762
1261
  verifySession
@@ -765,6 +1264,7 @@ var clerkAuthInternals = {
765
1264
  getBindings,
766
1265
  getConfiguredProviderConfig,
767
1266
  getSessionTokenFromRequest,
1267
+ normalizeClerkUserProfile,
768
1268
  normalizeHostedProfile,
769
1269
  resolveConfiguredProviderName,
770
1270
  resolveDisplayName,
@@ -777,7 +1277,11 @@ export {
777
1277
  authenticate,
778
1278
  clerkAuth,
779
1279
  clerkAuthInternals,
1280
+ completeClerkAuth,
780
1281
  configureClerkAuthRuntime,
1282
+ loginWithClerk,
1283
+ logoutWithClerk,
1284
+ registerWithClerk,
781
1285
  resetClerkAuthRuntime,
782
1286
  syncIdentity,
783
1287
  verifyRequest,