@holo-js/auth-clerk 0.1.4 → 0.1.5

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 +750 -223
  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,33 +90,146 @@ 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
234
  if (typeof exp === "number" && exp * 1e3 <= Date.now()) {
147
235
  throw new Error("[@holo-js/auth-clerk] Clerk token has expired.");
@@ -168,16 +256,24 @@ function normalizeClerkEmailAddress(value) {
168
256
  });
169
257
  }
170
258
  function normalizeClerkUserProfile(user) {
259
+ 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;
260
+ const primaryEmailAddressId = typeof user.primaryEmailAddressId === "string" ? user.primaryEmailAddressId : typeof user.primary_email_address_id === "string" ? user.primary_email_address_id : void 0;
261
+ const email = typeof user.email === "string" && user.email.trim() ? user.email.trim() : (emailAddresses?.find((entry) => entry.id === primaryEmailAddressId) ?? emailAddresses?.[0])?.emailAddress.trim() ?? "";
262
+ const emailVerified = user.emailVerified === true || user.email_verified === true || (emailAddresses?.find((entry) => entry.id === primaryEmailAddressId) ?? emailAddresses?.[0])?.verificationStatus === "verified";
263
+ const firstName = typeof user.firstName === "string" ? user.firstName : typeof user.first_name === "string" ? user.first_name : void 0;
264
+ const lastName = typeof user.lastName === "string" ? user.lastName : typeof user.last_name === "string" ? user.last_name : void 0;
265
+ const explicitName = typeof user.name === "string" ? user.name.trim() : "";
266
+ const name = explicitName || [firstName?.trim(), lastName?.trim()].filter(Boolean).join(" ").trim();
171
267
  return Object.freeze({
172
268
  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,
269
+ email,
270
+ emailVerified,
271
+ firstName,
272
+ lastName,
273
+ name: name || email || String(user.id ?? ""),
178
274
  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,
275
+ primaryEmailAddressId,
276
+ emailAddresses,
181
277
  raw: user
182
278
  });
183
279
  }
@@ -203,7 +299,6 @@ function createDefaultProviderRuntime(providerName, config) {
203
299
  providerName,
204
300
  config.apiUrl ?? "",
205
301
  config.frontendApi ?? "",
206
- config.jwtKey ?? "",
207
302
  config.secretKey ?? "",
208
303
  [...config.authorizedParties ?? []].sort()
209
304
  ]);
@@ -239,7 +334,11 @@ function resolveConfiguredProviderName(provider) {
239
334
  if (provider?.trim()) {
240
335
  return provider.trim();
241
336
  }
242
- const configuredProviders = Object.keys(bindings.config.clerk);
337
+ const configuredDefaultProvider = typeof bindings.config.clerk.provider === "string" ? bindings.config.clerk.provider.trim() : "";
338
+ if (configuredDefaultProvider) {
339
+ return configuredDefaultProvider;
340
+ }
341
+ const configuredProviders = Object.entries(bindings.config.clerk).flatMap(([name, value]) => name !== "provider" && name !== "identityStore" && isNormalizedClerkProviderConfig(value) ? [name] : []);
243
342
  if (configuredProviders.length === 0) {
244
343
  return "default";
245
344
  }
@@ -251,20 +350,23 @@ function resolveConfiguredProviderName(provider) {
251
350
  }
252
351
  throw new Error("[@holo-js/auth-clerk] Clerk provider name is required when multiple auth.clerk entries exist.");
253
352
  }
353
+ function isNormalizedClerkProviderConfig(value) {
354
+ return value !== null && typeof value === "object" && "sessionCookie" in value && typeof value.sessionCookie === "string" && "authorizedParties" in value && Array.isArray(value.authorizedParties);
355
+ }
254
356
  function getConfiguredProviderConfig(provider) {
255
357
  const providerName = resolveConfiguredProviderName(provider);
256
358
  const authBindings = authRuntimeInternals.getRuntimeBindings();
257
359
  const configured = authBindings.config.clerk[providerName];
258
- if (!configured) {
360
+ if (!isNormalizedClerkProviderConfig(configured)) {
259
361
  throw new Error(`[@holo-js/auth-clerk] Clerk provider "${providerName}" is not configured in auth.clerk.`);
260
362
  }
261
363
  return {
262
364
  name: providerName,
263
365
  publishableKey: configured.publishableKey,
264
366
  secretKey: configured.secretKey,
265
- jwtKey: configured.jwtKey,
266
367
  apiUrl: configured.apiUrl,
267
368
  frontendApi: configured.frontendApi,
369
+ redirectUri: configured.redirectUri,
268
370
  sessionCookie: configured.sessionCookie,
269
371
  authorizedParties: configured.authorizedParties ?? [],
270
372
  guard: configured.guard,
@@ -275,6 +377,73 @@ function getProviderRuntime(provider) {
275
377
  const providerName = resolveConfiguredProviderName(provider);
276
378
  return getBindings().providers[providerName] ?? createDefaultProviderRuntime(providerName, getConfiguredProviderConfig(providerName));
277
379
  }
380
+ function requireClerkHostedConfig(config) {
381
+ const frontendApi = config.frontendApi?.trim();
382
+ const redirectUri = config.redirectUri?.trim();
383
+ if (!frontendApi) {
384
+ throw new Error("[@holo-js/auth-clerk] Clerk hosted auth requires frontendApi to be configured.");
385
+ }
386
+ if (!redirectUri) {
387
+ throw new Error("[@holo-js/auth-clerk] Clerk hosted auth requires redirectUri to be configured.");
388
+ }
389
+ return { frontendApi, redirectUri };
390
+ }
391
+ function resolveClerkAccountPortalUrl(frontendApi) {
392
+ const url = new URL(frontendApi);
393
+ if (url.hostname.endsWith(".clerk.accounts.dev")) {
394
+ url.hostname = `${url.hostname.slice(0, -".clerk.accounts.dev".length)}.accounts.dev`;
395
+ } else if (url.hostname.startsWith("clerk.")) {
396
+ url.hostname = `accounts.${url.hostname.slice("clerk.".length)}`;
397
+ }
398
+ url.pathname = "/";
399
+ url.search = "";
400
+ url.hash = "";
401
+ return url.toString();
402
+ }
403
+ function createAuthorizationUrl(config, page) {
404
+ const { frontendApi, redirectUri } = requireClerkHostedConfig(config);
405
+ const portalBaseUrl = resolveClerkAccountPortalUrl(frontendApi);
406
+ const url = new URL(page, portalBaseUrl);
407
+ url.searchParams.set("redirect_url", redirectUri);
408
+ return url.toString();
409
+ }
410
+ function createClerkReturnUrl(request, returnTo) {
411
+ const requestUrl = new URL(request.url);
412
+ const safeDefault = new URL("/", requestUrl).toString();
413
+ const trimmed = returnTo?.trim();
414
+ if (!trimmed || hasControlCharacter(trimmed)) {
415
+ return safeDefault;
416
+ }
417
+ try {
418
+ const parsed = new URL(trimmed, requestUrl);
419
+ const isRelativePath = trimmed.startsWith("/") && !trimmed.startsWith("//");
420
+ const isAbsoluteUrl = /^[a-z][a-z\d+.-]*:/iu.test(trimmed);
421
+ if (parsed.origin !== requestUrl.origin || !parsed.pathname.startsWith("/") || !isRelativePath && !isAbsoluteUrl) {
422
+ return safeDefault;
423
+ }
424
+ return parsed.toString();
425
+ } catch {
426
+ return safeDefault;
427
+ }
428
+ }
429
+ function hasControlCharacter(value) {
430
+ for (const character of value) {
431
+ const codePoint = character.codePointAt(0);
432
+ if (typeof codePoint === "number" && (codePoint < 32 || codePoint === 127)) {
433
+ return true;
434
+ }
435
+ }
436
+ return false;
437
+ }
438
+ function createClerkErrorResponse(error, code) {
439
+ return Response.json({
440
+ ok: false,
441
+ code,
442
+ message: getErrorMessage(error)
443
+ }, {
444
+ status: error instanceof ClerkAuthConflictError ? 409 : 422
445
+ });
446
+ }
278
447
  function resolveGuardAndProvider(provider) {
279
448
  const authBindings = authRuntimeInternals.getRuntimeBindings();
280
449
  const providerConfig = getConfiguredProviderConfig(provider);
@@ -313,6 +482,46 @@ function requireUserRecord(user, message) {
313
482
  }
314
483
  return user;
315
484
  }
485
+ function requireSerializedUser(user, message) {
486
+ if (typeof user.id === "string" || typeof user.id === "number") {
487
+ return user;
488
+ }
489
+ throw new Error(message);
490
+ }
491
+ function createClerkSessionPayload(authenticated, session) {
492
+ const user = requireSerializedUser(
493
+ authenticated.user,
494
+ "[@holo-js/auth-clerk] Clerk-authenticated local users must expose a serializable id."
495
+ );
496
+ return Object.freeze({
497
+ guard: authenticated.guard,
498
+ provider: authenticated.authProvider,
499
+ userId: user.id,
500
+ user,
501
+ clerk: Object.freeze({
502
+ provider: authenticated.provider,
503
+ sessionId: session.sessionId
504
+ })
505
+ });
506
+ }
507
+ function getClerkLogoutSession(payload, providerName) {
508
+ if (!payload || typeof payload !== "object" || !("clerk" in payload)) {
509
+ return null;
510
+ }
511
+ const clerk = payload.clerk;
512
+ if (!clerk || typeof clerk !== "object") {
513
+ return null;
514
+ }
515
+ const provider = clerk.provider;
516
+ const sessionId = clerk.sessionId;
517
+ if (provider !== providerName || typeof sessionId !== "string" || !sessionId.trim()) {
518
+ return null;
519
+ }
520
+ return Object.freeze({
521
+ provider,
522
+ sessionId
523
+ });
524
+ }
316
525
  function serializeLocalUser(adapter, user, providerName) {
317
526
  const id = adapter.getId(user);
318
527
  const serialized = adapter.serialize ? adapter.serialize(user) : { ...user };
@@ -325,6 +534,11 @@ function serializeLocalUser(adapter, user, providerName) {
325
534
  enumerable: false,
326
535
  configurable: true
327
536
  });
537
+ Object.defineProperty(result, "can", {
538
+ value: () => false,
539
+ enumerable: false,
540
+ configurable: true
541
+ });
328
542
  return Object.freeze(result);
329
543
  }
330
544
  function resolvePrimaryEmail(profile) {
@@ -351,7 +565,7 @@ function resolveDisplayName(profile) {
351
565
  return profile.name.trim();
352
566
  }
353
567
  const fullName = [profile.firstName?.trim(), profile.lastName?.trim()].filter(Boolean).join(" ").trim();
354
- return fullName || void 0;
568
+ return fullName || profile.email || profile.id;
355
569
  }
356
570
  function resolveEmailForCreation(profile) {
357
571
  const primary = resolvePrimaryEmail(profile).email;
@@ -360,6 +574,34 @@ function resolveEmailForCreation(profile) {
360
574
  }
361
575
  return `${profile.id}@clerk.hosted.local`;
362
576
  }
577
+ function toAdapterInput(input) {
578
+ return Object.freeze(Object.fromEntries(
579
+ Object.entries(input).filter((entry) => typeof entry[1] !== "undefined")
580
+ ));
581
+ }
582
+ function withRequiredUserFields(input, profile) {
583
+ return Object.freeze({
584
+ ...input,
585
+ email: typeof input.email === "undefined" ? resolveEmailForCreation(profile) : input.email,
586
+ name: typeof input.name === "undefined" ? resolveDisplayName(profile) : input.name
587
+ });
588
+ }
589
+ function resolveCreateUserInput(profile, mapper) {
590
+ return toAdapterInput(withRequiredUserFields({
591
+ password: null,
592
+ avatar: profile.imageUrl ?? null,
593
+ email_verified_at: profile.emailVerified ? /* @__PURE__ */ new Date() : null,
594
+ ...mapper?.(profile) ?? {}
595
+ }, profile));
596
+ }
597
+ function resolveUpdateUserInput(profile, mapper) {
598
+ return toAdapterInput(withRequiredUserFields({
599
+ email: profile.email.trim() || void 0,
600
+ avatar: profile.imageUrl,
601
+ email_verified_at: profile.emailVerified ? /* @__PURE__ */ new Date() : void 0,
602
+ ...mapper?.(profile) ?? {}
603
+ }, profile));
604
+ }
363
605
  function normalizeHostedProfile(profile) {
364
606
  const resolvedEmail = resolvePrimaryEmail(profile);
365
607
  return Object.freeze({
@@ -383,21 +625,20 @@ async function findUserByEmail(adapter, email) {
383
625
  return user ? requireUserRecord(user, "[@holo-js/auth-clerk] Auth provider lookups must return object users.") : null;
384
626
  }
385
627
  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
628
  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;
629
+ const changed = Object.entries(input).some(([key, value]) => {
630
+ if (key === "email_verified_at") {
631
+ return value instanceof Date && !current.email_verified_at;
632
+ }
633
+ return !Object.is(value, user[key]);
634
+ });
394
635
  if (!changed) {
395
636
  return { user, changed: false };
396
637
  }
397
638
  if (adapter.update) {
398
639
  return {
399
640
  user: requireUserRecord(
400
- await adapter.update(user, nextInput),
641
+ await adapter.update(user, input),
401
642
  "[@holo-js/auth-clerk] Auth provider updates must return object users."
402
643
  ),
403
644
  changed: true
@@ -407,7 +648,7 @@ async function updateLocalUser(adapter, user, input) {
407
648
  "[@holo-js/auth-clerk] Auth provider adapters must implement update() to persist profile changes."
408
649
  );
409
650
  }
410
- async function ensureNoUnexpectedEmailCollision(adapter, authProvider, profile, currentUserId) {
651
+ async function ensureNoUnexpectedEmailCollision(adapter, providerName, profile, currentUserId) {
411
652
  const resolvedEmail = resolvePrimaryEmail(profile).email;
412
653
  if (!resolvedEmail) {
413
654
  return;
@@ -422,7 +663,7 @@ async function ensureNoUnexpectedEmailCollision(adapter, authProvider, profile,
422
663
  "[@holo-js/auth-clerk] Matched local users must expose a serializable id."
423
664
  ) !== currentUserId) {
424
665
  throw new ClerkAuthConflictError({
425
- provider: "clerk",
666
+ provider: providerName,
426
667
  clerkUserId: profile.id,
427
668
  email: resolvedEmail,
428
669
  message: `[@holo-js/auth-clerk] Clerk email "${resolvedEmail}" collides with a different local user.`
@@ -463,6 +704,47 @@ function createIdentityRecord(input) {
463
704
  updatedAt: now
464
705
  });
465
706
  }
707
+ async function withIdentitySyncLock(key, callback) {
708
+ const previous = clerkIdentitySyncLocks.get(key) ?? Promise.resolve();
709
+ let release = () => {
710
+ };
711
+ const current = previous.then(() => new Promise((resolve) => {
712
+ release = resolve;
713
+ }));
714
+ clerkIdentitySyncLocks.set(key, current);
715
+ await previous;
716
+ try {
717
+ return await callback();
718
+ } finally {
719
+ release();
720
+ if (clerkIdentitySyncLocks.get(key) === current) {
721
+ clerkIdentitySyncLocks.delete(key);
722
+ }
723
+ }
724
+ }
725
+ async function claimNewIdentity(identityStore, identity) {
726
+ if (identityStore.claim) {
727
+ return await identityStore.claim(identity);
728
+ }
729
+ await identityStore.save(identity);
730
+ return identity;
731
+ }
732
+ function sameUserId(left, right) {
733
+ return String(left) === String(right);
734
+ }
735
+ async function resolveClaimedIdentityUser(adapter, identity, fallback) {
736
+ if (sameUserId(identity.userId, fallback.userId)) {
737
+ return fallback.user;
738
+ }
739
+ const claimedUser = requireUserRecord(
740
+ await adapter.findById(identity.userId),
741
+ "[@holo-js/auth-clerk] Claimed Clerk identities must reference an existing local user."
742
+ );
743
+ if (fallback.deleteOnMismatch && adapter.delete) {
744
+ await adapter.delete(fallback.userId);
745
+ }
746
+ return claimedUser;
747
+ }
466
748
  function getSessionTokenFromRequest(request, sessionCookie) {
467
749
  const authorization = request.headers.get("authorization")?.trim();
468
750
  if (authorization) {
@@ -474,6 +756,53 @@ function getSessionTokenFromRequest(request, sessionCookie) {
474
756
  const cookies = parseCookieHeader(request.headers.get("cookie"));
475
757
  return cookies[sessionCookie] ?? null;
476
758
  }
759
+ function hasClerkCallbackState(request) {
760
+ const url = new URL(request.url);
761
+ return [...url.searchParams.keys()].some((key) => key.startsWith("__clerk"));
762
+ }
763
+ function hasClerkHandshakeAttempt(request) {
764
+ const url = new URL(request.url);
765
+ if (url.searchParams.has("__clerk_handshake") || url.searchParams.has("__clerk_handshake_nonce") || url.searchParams.has("__clerk_synced")) {
766
+ return true;
767
+ }
768
+ const cookies = parseCookieHeader(request.headers.get("cookie"));
769
+ const redirectCount = cookies.__clerk_redirect_count;
770
+ return typeof redirectCount === "string" && redirectCount !== "" && redirectCount !== "0";
771
+ }
772
+ function isConfiguredClerkRedirectRequest(request, config) {
773
+ const redirectUri = config.redirectUri?.trim();
774
+ if (!redirectUri) {
775
+ return hasClerkCallbackState(request);
776
+ }
777
+ try {
778
+ const requestUrl = new URL(request.url);
779
+ const configuredUrl = new URL(redirectUri, requestUrl.origin);
780
+ return requestUrl.origin === configuredUrl.origin && requestUrl.pathname === configuredUrl.pathname;
781
+ } catch {
782
+ return false;
783
+ }
784
+ }
785
+ function splitSetCookieHeader(value) {
786
+ return value.split(/,(?=\s*[^;,=\s]+=)/g).map((cookie) => cookie.trim()).filter(Boolean);
787
+ }
788
+ function getSetCookieHeaders(headers) {
789
+ const withGetSetCookie = headers;
790
+ const explicit = withGetSetCookie.getSetCookie?.();
791
+ if (explicit?.length) {
792
+ return explicit;
793
+ }
794
+ const combined = headers.get("set-cookie");
795
+ return combined ? splitSetCookieHeader(combined) : [];
796
+ }
797
+ async function appendClerkResponseCookies(headers) {
798
+ const append = authRuntimeInternals.getRuntimeBindings().context.appendResponseCookie;
799
+ if (!append) {
800
+ return;
801
+ }
802
+ for (const cookie of getSetCookieHeaders(headers)) {
803
+ await append(cookie);
804
+ }
805
+ }
477
806
  function getHoloSessionIdFromRequest(request) {
478
807
  const cookieHeader = authRuntimeInternals.getRuntimeBindings().session.sessionCookie("");
479
808
  const separator = cookieHeader.indexOf("=");
@@ -496,9 +825,16 @@ async function reuseExistingHoloSession(request, authenticated) {
496
825
  if (!payload || payload.guard !== authenticated.guard || payload.provider !== authenticated.authProvider || String(payload.userId) !== String(authenticated.user.id)) {
497
826
  return null;
498
827
  }
828
+ const source = payload;
829
+ if (source.clerk && typeof source.clerk === "object") {
830
+ if (typeof source.clerk.provider !== "string" || source.clerk.provider !== authenticated.provider) {
831
+ return null;
832
+ }
833
+ }
499
834
  bindings.context.setCachedUser(authenticated.guard, authenticated.user);
500
835
  return Object.freeze({
501
836
  guard: authenticated.guard,
837
+ provider: source.clerk && typeof source.clerk === "object" ? "clerk" : payload.provider,
502
838
  user: authenticated.user,
503
839
  sessionId,
504
840
  cookies: Object.freeze([])
@@ -516,7 +852,71 @@ async function verifySession(token, provider) {
516
852
  config: providerConfig
517
853
  });
518
854
  }
519
- async function verifyRequest(request, provider) {
855
+ function resolveRequestAuthorizedParties(config, request) {
856
+ return Object.freeze([
857
+ .../* @__PURE__ */ new Set([
858
+ ...config.authorizedParties ?? [],
859
+ new URL(request.url).origin
860
+ ])
861
+ ]);
862
+ }
863
+ async function verifyDefaultSessionToken(token, providerConfig, authorizedParties) {
864
+ const config = {
865
+ ...providerConfig,
866
+ authorizedParties
867
+ };
868
+ const defaultRuntime = createDefaultProviderRuntime(providerConfig.name, config);
869
+ if (!defaultRuntime.verifySession) {
870
+ throw new Error(`[@holo-js/auth-clerk] Clerk provider runtime "${providerConfig.name}" does not implement verifySession().`);
871
+ }
872
+ return defaultRuntime.verifySession({
873
+ provider: providerConfig.name,
874
+ token,
875
+ config
876
+ });
877
+ }
878
+ async function verifyRequestWithClerkBackendSdk(request, providerConfig) {
879
+ const publishableKey = providerConfig.publishableKey?.trim();
880
+ const secretKey = providerConfig.secretKey?.trim();
881
+ if (!publishableKey || !secretKey) {
882
+ return null;
883
+ }
884
+ const { createClerkClient } = await import("@clerk/backend");
885
+ const client = createClerkClient({
886
+ apiUrl: providerConfig.apiUrl,
887
+ publishableKey,
888
+ secretKey
889
+ });
890
+ const authenticateOptions = {
891
+ apiUrl: providerConfig.apiUrl,
892
+ authorizedParties: [...resolveRequestAuthorizedParties(providerConfig, request)],
893
+ publishableKey,
894
+ secretKey
895
+ };
896
+ const requestState = await client.authenticateRequest(request, authenticateOptions);
897
+ await appendClerkResponseCookies(requestState.headers);
898
+ if (requestState.status === "handshake") {
899
+ const redirectUrl = requestState.headers.get("location");
900
+ if (redirectUrl && !hasClerkHandshakeAttempt(request)) {
901
+ await authRuntimeInternals.redirectResponse(
902
+ authRuntimeInternals.getRuntimeBindings(),
903
+ new URL(redirectUrl, request.url).toString(),
904
+ 307
905
+ );
906
+ }
907
+ return null;
908
+ }
909
+ if (requestState.status !== "signed-in") {
910
+ return null;
911
+ }
912
+ return verifyDefaultSessionToken(
913
+ requestState.token,
914
+ providerConfig,
915
+ resolveRequestAuthorizedParties(providerConfig, request)
916
+ );
917
+ }
918
+ async function verifyRequest(input, provider) {
919
+ const request = normalizeClerkRequest(input);
520
920
  const providerConfig = getConfiguredProviderConfig(provider);
521
921
  const configuredRuntime = getBindings().providers[providerConfig.name];
522
922
  const runtime = configuredRuntime ?? getProviderRuntime(providerConfig.name);
@@ -528,6 +928,12 @@ async function verifyRequest(request, provider) {
528
928
  });
529
929
  }
530
930
  const token = getSessionTokenFromRequest(request, providerConfig.sessionCookie);
931
+ if (!configuredRuntime && (hasClerkCallbackState(request) || isConfiguredClerkRedirectRequest(request, providerConfig))) {
932
+ const clerkSession = await verifyRequestWithClerkBackendSdk(request, providerConfig);
933
+ if (clerkSession) {
934
+ return clerkSession;
935
+ }
936
+ }
531
937
  if (!token) {
532
938
  return null;
533
939
  }
@@ -541,34 +947,9 @@ async function verifyRequest(request, provider) {
541
947
  config: providerConfig
542
948
  });
543
949
  }
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
- });
950
+ return verifyDefaultSessionToken(token, providerConfig, resolveRequestAuthorizedParties(providerConfig, request));
570
951
  }
571
- async function syncIdentity(session, provider) {
952
+ async function syncIdentity(session, provider, options = {}) {
572
953
  const providerConfig = getConfiguredProviderConfig(provider);
573
954
  const providerName = providerConfig.name;
574
955
  const profile = session.user;
@@ -579,155 +960,147 @@ async function syncIdentity(session, provider) {
579
960
  if (verificationRequired && !verifiedEmail) {
580
961
  throw new Error(`[@holo-js/auth-clerk] Clerk identity "${profile.id}" must provide a verified email address.`);
581
962
  }
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
- }
963
+ return await withIdentitySyncLock(`${providerName}:${profile.id}`, async () => {
964
+ const identityStore = getBindings().identityStore;
965
+ const existingIdentity = await identityStore.findByProviderUserId(providerName, profile.id);
966
+ if (existingIdentity) {
967
+ const existingLinkedUser = await adapter.findById(existingIdentity.userId);
968
+ let linkedUser = existingLinkedUser ? requireUserRecord(existingLinkedUser, "[@holo-js/auth-clerk] Auth provider lookups must return object users.") : null;
592
969
  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.");
970
+ linkedUser = verifiedEmail ? await findUserByEmail(adapter, verifiedEmail) : null;
971
+ if (linkedUser) {
972
+ await assertUserLinkAvailable(providerName, authProvider, adapter, linkedUser, profile.id);
973
+ }
974
+ if (!linkedUser) {
975
+ linkedUser = requireUserRecord(
976
+ await adapter.create(resolveCreateUserInput(profile, options.user)),
977
+ "[@holo-js/auth-clerk] Auth provider create() must return an object user."
978
+ );
979
+ }
980
+ const relinked = await updateLocalUser(adapter, linkedUser, resolveUpdateUserInput(profile, options.user));
981
+ const relinkedUser = relinked.user;
982
+ const identity3 = createIdentityRecord({
983
+ provider: providerName,
984
+ guard,
985
+ authProvider,
986
+ userId: requireUserId(
987
+ adapter,
988
+ relinkedUser,
989
+ "[@holo-js/auth-clerk] Relinked local users must expose a serializable id."
990
+ ),
991
+ profile,
992
+ previous: existingIdentity
993
+ });
994
+ await identityStore.save(identity3);
995
+ return Object.freeze({
996
+ provider: providerName,
997
+ guard,
998
+ authProvider,
999
+ status: "relinked",
1000
+ user: serializeLocalUser(adapter, relinkedUser, authProvider),
1001
+ identity: identity3,
1002
+ session
1003
+ });
600
1004
  }
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({
1005
+ await ensureNoUnexpectedEmailCollision(
1006
+ adapter,
1007
+ providerName,
1008
+ profile,
1009
+ requireUserId(
1010
+ adapter,
1011
+ linkedUser,
1012
+ "[@holo-js/auth-clerk] Linked local users must expose a serializable id."
1013
+ )
1014
+ );
1015
+ const updated = await updateLocalUser(adapter, linkedUser, resolveUpdateUserInput(profile, options.user));
1016
+ const identity2 = createIdentityRecord({
609
1017
  provider: providerName,
610
1018
  guard,
611
1019
  authProvider,
612
1020
  userId: requireUserId(
613
1021
  adapter,
614
- relinkedUser,
615
- "[@holo-js/auth-clerk] Relinked local users must expose a serializable id."
1022
+ updated.user,
1023
+ "[@holo-js/auth-clerk] Updated local users must expose a serializable id."
616
1024
  ),
617
1025
  profile,
618
1026
  previous: existingIdentity
619
1027
  });
620
- await identityStore.save(identity3);
1028
+ await identityStore.save(identity2);
621
1029
  return Object.freeze({
622
1030
  provider: providerName,
623
1031
  guard,
624
1032
  authProvider,
625
- status: "relinked",
626
- user: serializeLocalUser(adapter, relinkedUser, authProvider),
627
- identity: identity3,
1033
+ status: updated.changed ? "updated" : "linked",
1034
+ user: serializeLocalUser(adapter, updated.user, authProvider),
1035
+ identity: identity2,
628
1036
  session
629
1037
  });
630
1038
  }
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
- )
1039
+ let localUser = verifiedEmail ? await findUserByEmail(adapter, verifiedEmail) : null;
1040
+ if (localUser) {
1041
+ await assertUserLinkAvailable(providerName, authProvider, adapter, localUser, profile.id);
1042
+ const linked = await updateLocalUser(adapter, localUser, resolveUpdateUserInput(profile, options.user));
1043
+ const identity2 = createIdentityRecord({
1044
+ provider: providerName,
1045
+ guard,
1046
+ authProvider,
1047
+ userId: requireUserId(
1048
+ adapter,
1049
+ linked.user,
1050
+ "[@holo-js/auth-clerk] Linked local users must expose a serializable id."
1051
+ ),
1052
+ profile
1053
+ });
1054
+ const claimedIdentity2 = await claimNewIdentity(identityStore, identity2);
1055
+ const claimedUser2 = await resolveClaimedIdentityUser(adapter, claimedIdentity2, {
1056
+ user: linked.user,
1057
+ userId: identity2.userId
1058
+ });
1059
+ return Object.freeze({
1060
+ provider: providerName,
1061
+ guard,
1062
+ authProvider,
1063
+ status: "linked",
1064
+ user: serializeLocalUser(adapter, claimedUser2, authProvider),
1065
+ identity: claimedIdentity2,
1066
+ session
1067
+ });
1068
+ }
1069
+ localUser = requireUserRecord(
1070
+ await adapter.create(resolveCreateUserInput(profile, options.user)),
1071
+ "[@holo-js/auth-clerk] Auth provider create() must return an object user."
640
1072
  );
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({
1073
+ const identity = createIdentityRecord({
648
1074
  provider: providerName,
649
1075
  guard,
650
1076
  authProvider,
651
1077
  userId: requireUserId(
652
1078
  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."
1079
+ localUser,
1080
+ "[@holo-js/auth-clerk] Created local users must expose a serializable id."
687
1081
  ),
688
1082
  profile
689
1083
  });
690
- await identityStore.save(identity2);
1084
+ const claimedIdentity = await claimNewIdentity(identityStore, identity);
1085
+ const claimedUser = await resolveClaimedIdentityUser(adapter, claimedIdentity, {
1086
+ user: localUser,
1087
+ userId: identity.userId,
1088
+ deleteOnMismatch: true
1089
+ });
1090
+ const claimedStatus = sameUserId(claimedIdentity.userId, identity.userId) ? "created" : "linked";
691
1091
  return Object.freeze({
692
1092
  provider: providerName,
693
1093
  guard,
694
1094
  authProvider,
695
- status: "linked",
696
- user: serializeLocalUser(adapter, linked.user, authProvider),
697
- identity: identity2,
1095
+ status: claimedStatus,
1096
+ user: serializeLocalUser(adapter, claimedUser, authProvider),
1097
+ identity: claimedIdentity,
698
1098
  session
699
1099
  });
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
1100
  });
729
1101
  }
730
- async function authenticate(request, provider) {
1102
+ async function authenticate(input, provider) {
1103
+ const request = normalizeClerkRequest(input);
731
1104
  const session = await verifyRequest(request, provider);
732
1105
  if (!session) {
733
1106
  return null;
@@ -735,28 +1108,177 @@ async function authenticate(request, provider) {
735
1108
  const authenticated = await syncIdentity(session, provider);
736
1109
  const authSession = await reuseExistingHoloSession(request, authenticated) ?? await authRuntimeInternals.establishSessionForUser(authenticated.user, {
737
1110
  guard: authenticated.guard,
738
- provider: authenticated.authProvider
1111
+ provider: authenticated.authProvider,
1112
+ payload: createClerkSessionPayload(authenticated, session)
739
1113
  });
740
1114
  return Object.freeze({
741
1115
  ...authenticated,
742
1116
  authSession
743
1117
  });
744
1118
  }
1119
+ function getErrorMessage(error) {
1120
+ return error instanceof Error ? error.message : "Clerk authentication failed.";
1121
+ }
1122
+ function createHostedAuthSuccess(data) {
1123
+ return Object.freeze({
1124
+ data,
1125
+ error: null
1126
+ });
1127
+ }
1128
+ function createHostedAuthFailure(code, message, status) {
1129
+ return Object.freeze({
1130
+ data: null,
1131
+ error: Object.freeze({
1132
+ code,
1133
+ message,
1134
+ status,
1135
+ fields: Object.freeze({
1136
+ _root: Object.freeze([message])
1137
+ })
1138
+ })
1139
+ });
1140
+ }
1141
+ async function loginWithClerk(_input, options = {}) {
1142
+ try {
1143
+ const providerConfig = getConfiguredProviderConfig(options.provider);
1144
+ return Response.redirect(createAuthorizationUrl(providerConfig, "sign-in"), 302);
1145
+ } catch (error) {
1146
+ return createClerkErrorResponse(error, "clerk_login_failed");
1147
+ }
1148
+ }
1149
+ async function registerWithClerk(_input, options = {}) {
1150
+ try {
1151
+ const providerConfig = getConfiguredProviderConfig(options.provider);
1152
+ return Response.redirect(createAuthorizationUrl(providerConfig, "sign-up"), 302);
1153
+ } catch (error) {
1154
+ return createClerkErrorResponse(error, "clerk_register_failed");
1155
+ }
1156
+ }
1157
+ async function readCurrentClerkLogoutSession(guard, providerName) {
1158
+ const bindings = authRuntimeInternals.getRuntimeBindings();
1159
+ const sessionId = bindings.context.getSessionId(guard);
1160
+ if (!sessionId) {
1161
+ return null;
1162
+ }
1163
+ const record = await bindings.session.read(sessionId);
1164
+ return getClerkLogoutSession(authRuntimeInternals.readSessionPayload(record, guard), providerName);
1165
+ }
1166
+ async function revokeClerkSession(config, sessionId) {
1167
+ const secretKey = config.secretKey?.trim();
1168
+ if (!secretKey) {
1169
+ throw new Error("[@holo-js/auth-clerk] Clerk logout requires secretKey to be configured.");
1170
+ }
1171
+ const apiBase = config.apiUrl?.trim() || CLERK_API_BASE_URL;
1172
+ const response = await fetch(`${apiBase.replace(/\/$/, "")}/v1/sessions/${encodeURIComponent(sessionId)}/revoke`, {
1173
+ method: "POST",
1174
+ headers: {
1175
+ accept: "application/json",
1176
+ authorization: `Bearer ${secretKey}`
1177
+ }
1178
+ });
1179
+ if (!response.ok) {
1180
+ throw new Error(`[@holo-js/auth-clerk] Failed to revoke Clerk session "${sessionId}".`);
1181
+ }
1182
+ }
1183
+ async function logoutWithClerk(input, options = {}) {
1184
+ const request = normalizeClerkRequest(input);
1185
+ try {
1186
+ const providerConfig = getConfiguredProviderConfig(options.provider);
1187
+ const providerName = providerConfig.name;
1188
+ const { guard } = resolveGuardAndProvider(providerName);
1189
+ const requestSessionId = getHoloSessionIdFromRequest(request);
1190
+ if (requestSessionId) {
1191
+ authRuntimeInternals.getRuntimeBindings().context.setSessionId(guard, requestSessionId);
1192
+ }
1193
+ const clerkSession = await readCurrentClerkLogoutSession(guard, providerName);
1194
+ if (!clerkSession) {
1195
+ return createHostedAuthFailure(
1196
+ "clerk_session_missing",
1197
+ "The current Holo session was not created by Clerk.",
1198
+ 422
1199
+ );
1200
+ }
1201
+ await revokeClerkSession(providerConfig, clerkSession.sessionId);
1202
+ const local = await getAuthRuntime().guard(guard).logout();
1203
+ return createHostedAuthSuccess(Object.freeze({
1204
+ url: createClerkReturnUrl(request, options.returnTo),
1205
+ local
1206
+ }));
1207
+ } catch {
1208
+ return createHostedAuthFailure("clerk_logout_failed", "Unable to complete Clerk logout.", 500);
1209
+ }
1210
+ }
1211
+ async function completeClerkAuth(input, options = {}) {
1212
+ try {
1213
+ const request = normalizeClerkRequest(input);
1214
+ const url = new URL(request.url);
1215
+ const callbackError = url.searchParams.get("error")?.trim();
1216
+ if (callbackError) {
1217
+ return createHostedAuthFailure(
1218
+ callbackError,
1219
+ url.searchParams.get("error_description")?.trim() || "Clerk authentication failed.",
1220
+ 422
1221
+ );
1222
+ }
1223
+ const providerConfig = getConfiguredProviderConfig(options.provider);
1224
+ const session = await verifyRequest(request, providerConfig.name);
1225
+ if (!session) {
1226
+ return createHostedAuthFailure(
1227
+ "clerk_session_required",
1228
+ "Clerk callback did not include an active session.",
1229
+ 422
1230
+ );
1231
+ }
1232
+ const authenticated = await syncIdentity(session, providerConfig.name, {
1233
+ user: options.user
1234
+ });
1235
+ const authSession = await authRuntimeInternals.establishSessionForUser(authenticated.user, {
1236
+ guard: authenticated.guard,
1237
+ provider: authenticated.authProvider,
1238
+ payload: createClerkSessionPayload(authenticated, session)
1239
+ });
1240
+ return createHostedAuthSuccess(Object.freeze({
1241
+ provider: authenticated.provider,
1242
+ guard: authenticated.guard,
1243
+ authProvider: authenticated.authProvider,
1244
+ status: authenticated.status,
1245
+ user: authenticated.user,
1246
+ identity: authenticated.identity,
1247
+ session,
1248
+ authSession
1249
+ }));
1250
+ } catch (error) {
1251
+ if (authRuntimeInternals.isResponseInterrupt(error)) {
1252
+ throw error;
1253
+ }
1254
+ return createHostedAuthFailure(
1255
+ error instanceof ClerkAuthConflictError ? error.code : "clerk_auth_failed",
1256
+ getErrorMessage(error),
1257
+ error instanceof ClerkAuthConflictError ? 409 : 422
1258
+ );
1259
+ }
1260
+ }
745
1261
  function configureClerkAuthRuntime(bindings) {
1262
+ const runtimeState = getRuntimeState();
746
1263
  if (!bindings) {
747
- clerkBindings = void 0;
1264
+ runtimeState.bindings = void 0;
748
1265
  return;
749
1266
  }
750
- clerkBindings = Object.freeze({
751
- providers: bindings.providers ?? clerkBindings?.providers,
752
- identityStore: bindings.identityStore ?? clerkBindings?.identityStore
1267
+ runtimeState.bindings = Object.freeze({
1268
+ providers: bindings.providers ?? runtimeState.bindings?.providers,
1269
+ identityStore: bindings.identityStore ?? runtimeState.bindings?.identityStore
753
1270
  });
754
1271
  }
755
1272
  function resetClerkAuthRuntime() {
756
- clerkBindings = void 0;
1273
+ getRuntimeState().bindings = void 0;
1274
+ clerkDefaultProviderRuntimeCache.clear();
757
1275
  }
758
1276
  var clerkAuth = Object.freeze({
759
1277
  authenticate,
1278
+ completeClerkAuth,
1279
+ loginWithClerk,
1280
+ logoutWithClerk,
1281
+ registerWithClerk,
760
1282
  syncIdentity,
761
1283
  verifyRequest,
762
1284
  verifySession
@@ -765,6 +1287,7 @@ var clerkAuthInternals = {
765
1287
  getBindings,
766
1288
  getConfiguredProviderConfig,
767
1289
  getSessionTokenFromRequest,
1290
+ normalizeClerkUserProfile,
768
1291
  normalizeHostedProfile,
769
1292
  resolveConfiguredProviderName,
770
1293
  resolveDisplayName,
@@ -777,7 +1300,11 @@ export {
777
1300
  authenticate,
778
1301
  clerkAuth,
779
1302
  clerkAuthInternals,
1303
+ completeClerkAuth,
780
1304
  configureClerkAuthRuntime,
1305
+ loginWithClerk,
1306
+ logoutWithClerk,
1307
+ registerWithClerk,
781
1308
  resetClerkAuthRuntime,
782
1309
  syncIdentity,
783
1310
  verifyRequest,