@holo-js/auth-clerk 0.1.3

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.
@@ -0,0 +1,151 @@
1
+ import { AuthUserLike, AuthEstablishedSession, authRuntimeInternals } from '@holo-js/auth';
2
+ import { AuthClerkProviderConfig } from '@holo-js/config';
3
+
4
+ interface ClerkEmailAddress {
5
+ readonly id?: string;
6
+ readonly emailAddress: string;
7
+ readonly verificationStatus?: 'verified' | 'unverified' | 'pending';
8
+ }
9
+ interface ClerkUserProfile {
10
+ readonly id: string;
11
+ readonly email?: string;
12
+ readonly emailVerified?: boolean;
13
+ readonly firstName?: string;
14
+ readonly lastName?: string;
15
+ readonly name?: string;
16
+ readonly imageUrl?: string;
17
+ readonly primaryEmailAddressId?: string;
18
+ readonly emailAddresses?: readonly ClerkEmailAddress[];
19
+ readonly raw?: unknown;
20
+ }
21
+ interface ClerkVerifiedSession {
22
+ readonly sessionId: string;
23
+ readonly user: ClerkUserProfile;
24
+ readonly accessToken?: string;
25
+ readonly actor?: {
26
+ readonly id?: string;
27
+ readonly type?: string;
28
+ };
29
+ readonly raw?: unknown;
30
+ }
31
+ interface ClerkVerifyRequestContext {
32
+ readonly provider: string;
33
+ readonly request: Request;
34
+ readonly config: AuthClerkProviderConfig;
35
+ }
36
+ interface ClerkVerifySessionContext {
37
+ readonly provider: string;
38
+ readonly token: string;
39
+ readonly config: AuthClerkProviderConfig;
40
+ }
41
+ interface ClerkProviderRuntime {
42
+ verifyRequest?(context: ClerkVerifyRequestContext): Promise<ClerkVerifiedSession | null>;
43
+ verifySession?(context: ClerkVerifySessionContext): Promise<ClerkVerifiedSession | null>;
44
+ }
45
+ type RuntimeAuthProviderAdapter = ReturnType<typeof authRuntimeInternals.getRuntimeBindings>['providers'][string];
46
+ interface HostedIdentityRecord {
47
+ readonly provider: string;
48
+ readonly providerUserId: string;
49
+ readonly guard: string;
50
+ readonly authProvider: string;
51
+ readonly userId: string | number;
52
+ readonly email?: string;
53
+ readonly emailVerified: boolean;
54
+ readonly profile: Readonly<Record<string, unknown>>;
55
+ readonly linkedAt: Date;
56
+ readonly updatedAt: Date;
57
+ }
58
+ interface HostedIdentityStore {
59
+ findByProviderUserId(provider: string, providerUserId: string): Promise<HostedIdentityRecord | null>;
60
+ findByUserId(provider: string, authProvider: string, userId: string | number): Promise<HostedIdentityRecord | null>;
61
+ save(record: HostedIdentityRecord): Promise<void>;
62
+ }
63
+ type ClerkSyncStatus = 'created' | 'updated' | 'linked' | 'relinked';
64
+ interface ClerkAuthenticationResult {
65
+ readonly provider: string;
66
+ readonly guard: string;
67
+ readonly authProvider: string;
68
+ readonly status: ClerkSyncStatus;
69
+ readonly user: AuthUserLike;
70
+ readonly identity: HostedIdentityRecord;
71
+ readonly session: ClerkVerifiedSession;
72
+ readonly authSession?: AuthEstablishedSession;
73
+ }
74
+ interface ClerkAuthBindings {
75
+ readonly providers: Readonly<Record<string, ClerkProviderRuntime>>;
76
+ readonly identityStore: HostedIdentityStore;
77
+ }
78
+ interface ConfigureClerkAuthRuntimeOptions {
79
+ readonly providers?: Readonly<Record<string, ClerkProviderRuntime>>;
80
+ readonly identityStore?: HostedIdentityStore;
81
+ }
82
+ interface ClerkAuthFacade {
83
+ verifyRequest(request: Request, provider?: string): Promise<ClerkVerifiedSession | null>;
84
+ verifySession(token: string, provider?: string): Promise<ClerkVerifiedSession | null>;
85
+ syncIdentity(session: ClerkVerifiedSession, provider?: string): Promise<ClerkAuthenticationResult>;
86
+ authenticate(request: Request, provider?: string): Promise<ClerkAuthenticationResult | null>;
87
+ }
88
+ declare class ClerkAuthConflictError extends Error {
89
+ readonly code = "clerk_identity_conflict";
90
+ readonly provider: string;
91
+ readonly clerkUserId: string;
92
+ readonly email?: string;
93
+ constructor(options: {
94
+ readonly provider: string;
95
+ readonly clerkUserId: string;
96
+ readonly email?: string;
97
+ readonly message: string;
98
+ });
99
+ }
100
+ declare function getBindings(): ClerkAuthBindings;
101
+ declare function resolveConfiguredProviderName(provider?: string): string;
102
+ declare function getConfiguredProviderConfig(provider?: string): {
103
+ readonly name: string;
104
+ readonly publishableKey?: string;
105
+ readonly secretKey?: string;
106
+ readonly jwtKey?: string;
107
+ readonly apiUrl?: string;
108
+ readonly frontendApi?: string;
109
+ readonly sessionCookie: string;
110
+ readonly authorizedParties: readonly string[];
111
+ readonly guard?: string;
112
+ readonly mapToProvider?: string;
113
+ };
114
+ declare function resolveGuardAndProvider(provider?: string): {
115
+ readonly guard: string;
116
+ readonly authProvider: string;
117
+ readonly adapter: RuntimeAuthProviderAdapter;
118
+ };
119
+ declare function resolvePrimaryEmail(profile: ClerkUserProfile): {
120
+ email?: string;
121
+ emailVerified: boolean;
122
+ };
123
+ declare function resolveDisplayName(profile: ClerkUserProfile): string | undefined;
124
+ declare function resolveEmailForCreation(profile: ClerkUserProfile): string;
125
+ declare function normalizeHostedProfile(profile: ClerkUserProfile): Readonly<Record<string, unknown>>;
126
+ declare function getSessionTokenFromRequest(request: Request, sessionCookie: string): string | null;
127
+ declare function verifySession(token: string, provider?: string): Promise<ClerkVerifiedSession | null>;
128
+ declare function verifyRequest(request: Request, provider?: string): Promise<ClerkVerifiedSession | null>;
129
+ declare function syncIdentity(session: ClerkVerifiedSession, provider?: string): Promise<ClerkAuthenticationResult>;
130
+ declare function authenticate(request: Request, provider?: string): Promise<ClerkAuthenticationResult | null>;
131
+ declare function configureClerkAuthRuntime(bindings?: ConfigureClerkAuthRuntimeOptions): void;
132
+ declare function resetClerkAuthRuntime(): void;
133
+ declare const clerkAuth: Readonly<{
134
+ authenticate: typeof authenticate;
135
+ syncIdentity: typeof syncIdentity;
136
+ verifyRequest: typeof verifyRequest;
137
+ verifySession: typeof verifySession;
138
+ }>;
139
+ declare const clerkAuthInternals: {
140
+ getBindings: typeof getBindings;
141
+ getConfiguredProviderConfig: typeof getConfiguredProviderConfig;
142
+ getSessionTokenFromRequest: typeof getSessionTokenFromRequest;
143
+ normalizeHostedProfile: typeof normalizeHostedProfile;
144
+ resolveConfiguredProviderName: typeof resolveConfiguredProviderName;
145
+ resolveDisplayName: typeof resolveDisplayName;
146
+ resolveEmailForCreation: typeof resolveEmailForCreation;
147
+ resolveGuardAndProvider: typeof resolveGuardAndProvider;
148
+ resolvePrimaryEmail: typeof resolvePrimaryEmail;
149
+ };
150
+
151
+ export { type ClerkAuthBindings, ClerkAuthConflictError, type ClerkAuthFacade, type ClerkAuthenticationResult, type ClerkEmailAddress, type ClerkProviderRuntime, type ClerkSyncStatus, type ClerkUserProfile, type ClerkVerifiedSession, type ClerkVerifyRequestContext, type ClerkVerifySessionContext, type ConfigureClerkAuthRuntimeOptions, type HostedIdentityRecord, type HostedIdentityStore, authenticate, clerkAuth, clerkAuthInternals, configureClerkAuthRuntime, resetClerkAuthRuntime, syncIdentity, verifyRequest, verifySession };
package/dist/index.mjs ADDED
@@ -0,0 +1,785 @@
1
+ // src/index.ts
2
+ import { createPublicKey, verify as verifySignature } from "crypto";
3
+ import { authRuntimeInternals } from "@holo-js/auth";
4
+ import { parseCookieHeader } from "@holo-js/session";
5
+ var ClerkAuthConflictError = class extends Error {
6
+ code = "clerk_identity_conflict";
7
+ provider;
8
+ clerkUserId;
9
+ email;
10
+ constructor(options) {
11
+ super(options.message);
12
+ this.name = "ClerkAuthConflictError";
13
+ this.provider = options.provider;
14
+ this.clerkUserId = options.clerkUserId;
15
+ this.email = options.email;
16
+ }
17
+ };
18
+ var clerkBindings;
19
+ var CLERK_API_BASE_URL = "https://api.clerk.com";
20
+ var clerkDefaultProviderRuntimeCache = /* @__PURE__ */ new Map();
21
+ 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
+ function decodeJwtSegment(value, label) {
36
+ try {
37
+ return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
38
+ } catch {
39
+ throw new Error(`[@holo-js/auth-clerk] Clerk token ${label} was not valid JSON.`);
40
+ }
41
+ }
42
+ function parseJwt(token) {
43
+ const segments = token.split(".");
44
+ if (segments.length !== 3 || !segments[0] || !segments[1] || !segments[2]) {
45
+ throw new Error("[@holo-js/auth-clerk] Clerk token was not a valid JWT.");
46
+ }
47
+ return {
48
+ header: decodeJwtSegment(segments[0], "header"),
49
+ payload: decodeJwtSegment(segments[1], "payload"),
50
+ signature: Buffer.from(segments[2], "base64url"),
51
+ signingInput: Buffer.from(`${segments[0]}.${segments[1]}`, "utf8")
52
+ };
53
+ }
54
+ function verifyJwtSignatureWithJwk(token, jwk) {
55
+ const algorithm = typeof token.header.alg === "string" ? token.header.alg : "";
56
+ const key = createPublicKey({ key: jwk, format: "jwk" });
57
+ switch (algorithm) {
58
+ case "RS256":
59
+ return verifySignature("RSA-SHA256", token.signingInput, key, token.signature);
60
+ case "RS384":
61
+ return verifySignature("RSA-SHA384", token.signingInput, key, token.signature);
62
+ case "RS512":
63
+ return verifySignature("RSA-SHA512", token.signingInput, key, token.signature);
64
+ default:
65
+ throw new Error(`[@holo-js/auth-clerk] Unsupported Clerk JWT algorithm "${algorithm || "unknown"}".`);
66
+ }
67
+ }
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
+ function resolveClerkJwksUrl(config) {
83
+ const frontendApi = config.frontendApi?.trim();
84
+ if (frontendApi) {
85
+ return `${frontendApi.replace(/\/$/, "")}/.well-known/jwks.json`;
86
+ }
87
+ const apiUrl = config.apiUrl?.trim() || CLERK_API_BASE_URL;
88
+ return `${apiUrl.replace(/\/$/, "")}/v1/jwks`;
89
+ }
90
+ async function fetchClerkJwks(jwksUrl, options = {}) {
91
+ if (options.refresh) {
92
+ clerkJwksCache.delete(jwksUrl);
93
+ }
94
+ const existing = clerkJwksCache.get(jwksUrl);
95
+ if (existing) {
96
+ return existing;
97
+ }
98
+ const pending = (async () => {
99
+ const response = await fetch(jwksUrl, {
100
+ headers: {
101
+ accept: "application/json"
102
+ }
103
+ });
104
+ if (!response.ok) {
105
+ throw new Error(`[@holo-js/auth-clerk] Failed to load Clerk JWKS from "${jwksUrl}".`);
106
+ }
107
+ const payload = await response.json();
108
+ return payload.keys ?? [];
109
+ })();
110
+ clerkJwksCache.set(jwksUrl, pending);
111
+ try {
112
+ return await pending;
113
+ } catch (error) {
114
+ clerkJwksCache.delete(jwksUrl);
115
+ throw error;
116
+ }
117
+ }
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);
124
+ }
125
+ return void 0;
126
+ })();
127
+ if (verified === false) {
128
+ throw new Error("[@holo-js/auth-clerk] Clerk token signature verification failed.");
129
+ }
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);
140
+ }
141
+ if (!key || !verifyJwtSignatureWithJwk(parsed, key)) {
142
+ throw new Error("[@holo-js/auth-clerk] Clerk token signature verification failed.");
143
+ }
144
+ }
145
+ const exp = typeof parsed.payload.exp === "number" ? parsed.payload.exp : void 0;
146
+ if (typeof exp === "number" && exp * 1e3 <= Date.now()) {
147
+ throw new Error("[@holo-js/auth-clerk] Clerk token has expired.");
148
+ }
149
+ const nbf = typeof parsed.payload.nbf === "number" ? parsed.payload.nbf : void 0;
150
+ if (typeof nbf === "number" && nbf * 1e3 > Date.now()) {
151
+ throw new Error("[@holo-js/auth-clerk] Clerk token is not valid yet.");
152
+ }
153
+ 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
+ }
159
+ }
160
+ return parsed.payload;
161
+ }
162
+ function normalizeClerkEmailAddress(value) {
163
+ const verification = value.verification && typeof value.verification === "object" ? value.verification : void 0;
164
+ return Object.freeze({
165
+ id: typeof value.id === "string" ? value.id : void 0,
166
+ emailAddress: typeof value.emailAddress === "string" ? value.emailAddress : typeof value.email_address === "string" ? value.email_address : "",
167
+ verificationStatus: typeof value.verificationStatus === "string" ? value.verificationStatus : typeof verification?.status === "string" ? verification.status : void 0
168
+ });
169
+ }
170
+ function normalizeClerkUserProfile(user) {
171
+ return Object.freeze({
172
+ 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,
178
+ 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,
181
+ raw: user
182
+ });
183
+ }
184
+ async function fetchClerkUserProfile(userId, config) {
185
+ const secretKey = config.secretKey?.trim();
186
+ if (!secretKey) {
187
+ throw new Error("[@holo-js/auth-clerk] Clerk verification requires secretKey to be configured.");
188
+ }
189
+ const apiBase = config.apiUrl?.trim() || CLERK_API_BASE_URL;
190
+ const response = await fetch(`${apiBase.replace(/\/$/, "")}/v1/users/${encodeURIComponent(userId)}`, {
191
+ headers: {
192
+ accept: "application/json",
193
+ authorization: `Bearer ${secretKey}`
194
+ }
195
+ });
196
+ if (!response.ok) {
197
+ throw new Error(`[@holo-js/auth-clerk] Failed to load Clerk user "${userId}".`);
198
+ }
199
+ return normalizeClerkUserProfile(await response.json());
200
+ }
201
+ function createDefaultProviderRuntime(providerName, config) {
202
+ const cacheKey = JSON.stringify([
203
+ providerName,
204
+ config.apiUrl ?? "",
205
+ config.frontendApi ?? "",
206
+ config.jwtKey ?? "",
207
+ config.secretKey ?? "",
208
+ [...config.authorizedParties ?? []].sort()
209
+ ]);
210
+ const existing = clerkDefaultProviderRuntimeCache.get(cacheKey);
211
+ if (existing) {
212
+ return existing;
213
+ }
214
+ const runtime = Object.freeze({
215
+ async verifySession({ token }) {
216
+ const claims = await verifyClerkSessionToken(token, config, config.authorizedParties);
217
+ const userId = typeof claims.sub === "string" ? claims.sub : "";
218
+ if (!userId) {
219
+ throw new Error("[@holo-js/auth-clerk] Clerk token did not include a subject.");
220
+ }
221
+ const profile = await fetchClerkUserProfile(userId, config);
222
+ const sessionId = typeof claims.sid === "string" ? claims.sid : typeof claims.session_id === "string" ? claims.session_id : token;
223
+ return Object.freeze({
224
+ sessionId,
225
+ user: profile,
226
+ accessToken: token,
227
+ raw: Object.freeze({
228
+ claims,
229
+ user: profile.raw
230
+ })
231
+ });
232
+ }
233
+ });
234
+ clerkDefaultProviderRuntimeCache.set(cacheKey, runtime);
235
+ return runtime;
236
+ }
237
+ function resolveConfiguredProviderName(provider) {
238
+ const bindings = authRuntimeInternals.getRuntimeBindings();
239
+ if (provider?.trim()) {
240
+ return provider.trim();
241
+ }
242
+ const configuredProviders = Object.keys(bindings.config.clerk);
243
+ if (configuredProviders.length === 0) {
244
+ return "default";
245
+ }
246
+ if (configuredProviders.length === 1) {
247
+ return configuredProviders[0];
248
+ }
249
+ if (configuredProviders.includes("default")) {
250
+ return "default";
251
+ }
252
+ throw new Error("[@holo-js/auth-clerk] Clerk provider name is required when multiple auth.clerk entries exist.");
253
+ }
254
+ function getConfiguredProviderConfig(provider) {
255
+ const providerName = resolveConfiguredProviderName(provider);
256
+ const authBindings = authRuntimeInternals.getRuntimeBindings();
257
+ const configured = authBindings.config.clerk[providerName];
258
+ if (!configured) {
259
+ throw new Error(`[@holo-js/auth-clerk] Clerk provider "${providerName}" is not configured in auth.clerk.`);
260
+ }
261
+ return {
262
+ name: providerName,
263
+ publishableKey: configured.publishableKey,
264
+ secretKey: configured.secretKey,
265
+ jwtKey: configured.jwtKey,
266
+ apiUrl: configured.apiUrl,
267
+ frontendApi: configured.frontendApi,
268
+ sessionCookie: configured.sessionCookie,
269
+ authorizedParties: configured.authorizedParties ?? [],
270
+ guard: configured.guard,
271
+ mapToProvider: configured.mapToProvider
272
+ };
273
+ }
274
+ function getProviderRuntime(provider) {
275
+ const providerName = resolveConfiguredProviderName(provider);
276
+ return getBindings().providers[providerName] ?? createDefaultProviderRuntime(providerName, getConfiguredProviderConfig(providerName));
277
+ }
278
+ function resolveGuardAndProvider(provider) {
279
+ const authBindings = authRuntimeInternals.getRuntimeBindings();
280
+ const providerConfig = getConfiguredProviderConfig(provider);
281
+ const guardName = providerConfig.guard ?? authBindings.config.defaults.guard;
282
+ const guard = authBindings.config.guards[guardName];
283
+ if (!guard) {
284
+ throw new Error(`[@holo-js/auth-clerk] Guard "${guardName}" is not configured for Clerk provider "${providerConfig.name}".`);
285
+ }
286
+ if (guard.driver !== "session") {
287
+ throw new Error(`[@holo-js/auth-clerk] Clerk sign-in requires auth guard "${guardName}" to use the session driver.`);
288
+ }
289
+ const authProvider = providerConfig.mapToProvider ?? guard.provider;
290
+ const adapter = authBindings.providers[authProvider];
291
+ if (!adapter) {
292
+ throw new Error(`[@holo-js/auth-clerk] Auth provider runtime "${authProvider}" is not configured.`);
293
+ }
294
+ return {
295
+ guard: guardName,
296
+ authProvider,
297
+ adapter
298
+ };
299
+ }
300
+ function requireUserId(adapter, user, message) {
301
+ if (!user || typeof user !== "object") {
302
+ throw new Error(message);
303
+ }
304
+ const userId = adapter.getId(user);
305
+ if (typeof userId !== "string" && typeof userId !== "number") {
306
+ throw new Error(message);
307
+ }
308
+ return userId;
309
+ }
310
+ function requireUserRecord(user, message) {
311
+ if (!user || typeof user !== "object") {
312
+ throw new Error(message);
313
+ }
314
+ return user;
315
+ }
316
+ function serializeLocalUser(adapter, user, providerName) {
317
+ const id = adapter.getId(user);
318
+ const serialized = adapter.serialize ? adapter.serialize(user) : { ...user };
319
+ const result = {
320
+ ...serialized,
321
+ id
322
+ };
323
+ Object.defineProperty(result, AUTH_PROVIDER_MARKER, {
324
+ value: providerName,
325
+ enumerable: false,
326
+ configurable: true
327
+ });
328
+ return Object.freeze(result);
329
+ }
330
+ function resolvePrimaryEmail(profile) {
331
+ if (profile.email?.trim()) {
332
+ return {
333
+ email: profile.email.trim(),
334
+ emailVerified: profile.emailVerified === true
335
+ };
336
+ }
337
+ const primary = profile.emailAddresses?.find((entry) => entry.id === profile.primaryEmailAddressId) ?? profile.emailAddresses?.[0];
338
+ if (!primary?.emailAddress?.trim()) {
339
+ return {
340
+ email: void 0,
341
+ emailVerified: false
342
+ };
343
+ }
344
+ return {
345
+ email: primary.emailAddress.trim(),
346
+ emailVerified: primary.verificationStatus === "verified"
347
+ };
348
+ }
349
+ function resolveDisplayName(profile) {
350
+ if (profile.name?.trim()) {
351
+ return profile.name.trim();
352
+ }
353
+ const fullName = [profile.firstName?.trim(), profile.lastName?.trim()].filter(Boolean).join(" ").trim();
354
+ return fullName || void 0;
355
+ }
356
+ function resolveEmailForCreation(profile) {
357
+ const primary = resolvePrimaryEmail(profile).email;
358
+ if (primary) {
359
+ return primary;
360
+ }
361
+ return `${profile.id}@clerk.hosted.local`;
362
+ }
363
+ function normalizeHostedProfile(profile) {
364
+ const resolvedEmail = resolvePrimaryEmail(profile);
365
+ return Object.freeze({
366
+ id: profile.id,
367
+ email: resolvedEmail.email,
368
+ emailVerified: resolvedEmail.emailVerified,
369
+ firstName: profile.firstName,
370
+ lastName: profile.lastName,
371
+ name: resolveDisplayName(profile),
372
+ imageUrl: profile.imageUrl,
373
+ primaryEmailAddressId: profile.primaryEmailAddressId,
374
+ emailAddresses: profile.emailAddresses ? [...profile.emailAddresses] : void 0,
375
+ raw: profile.raw
376
+ });
377
+ }
378
+ async function findUserByEmail(adapter, email) {
379
+ if (!email?.trim()) {
380
+ return null;
381
+ }
382
+ const user = await adapter.findByCredentials({ email: email.trim() });
383
+ return user ? requireUserRecord(user, "[@holo-js/auth-clerk] Auth provider lookups must return object users.") : null;
384
+ }
385
+ 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
+ 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;
394
+ if (!changed) {
395
+ return { user, changed: false };
396
+ }
397
+ if (adapter.update) {
398
+ return {
399
+ user: requireUserRecord(
400
+ await adapter.update(user, nextInput),
401
+ "[@holo-js/auth-clerk] Auth provider updates must return object users."
402
+ ),
403
+ changed: true
404
+ };
405
+ }
406
+ throw new Error(
407
+ "[@holo-js/auth-clerk] Auth provider adapters must implement update() to persist profile changes."
408
+ );
409
+ }
410
+ async function ensureNoUnexpectedEmailCollision(adapter, authProvider, profile, currentUserId) {
411
+ const resolvedEmail = resolvePrimaryEmail(profile).email;
412
+ if (!resolvedEmail) {
413
+ return;
414
+ }
415
+ const matched = await findUserByEmail(adapter, resolvedEmail);
416
+ if (!matched) {
417
+ return;
418
+ }
419
+ if (requireUserId(
420
+ adapter,
421
+ matched,
422
+ "[@holo-js/auth-clerk] Matched local users must expose a serializable id."
423
+ ) !== currentUserId) {
424
+ throw new ClerkAuthConflictError({
425
+ provider: "clerk",
426
+ clerkUserId: profile.id,
427
+ email: resolvedEmail,
428
+ message: `[@holo-js/auth-clerk] Clerk email "${resolvedEmail}" collides with a different local user.`
429
+ });
430
+ }
431
+ }
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
+ function isEmailVerificationRequired() {
448
+ return authRuntimeInternals.getRuntimeBindings().config.emailVerification.required === true;
449
+ }
450
+ function createIdentityRecord(input) {
451
+ const now = /* @__PURE__ */ new Date();
452
+ const resolvedEmail = resolvePrimaryEmail(input.profile);
453
+ return Object.freeze({
454
+ provider: input.provider,
455
+ providerUserId: input.profile.id,
456
+ guard: input.guard,
457
+ authProvider: input.authProvider,
458
+ userId: input.userId,
459
+ email: resolvedEmail.email,
460
+ emailVerified: resolvedEmail.emailVerified,
461
+ profile: normalizeHostedProfile(input.profile),
462
+ linkedAt: input.previous?.linkedAt ?? now,
463
+ updatedAt: now
464
+ });
465
+ }
466
+ function getSessionTokenFromRequest(request, sessionCookie) {
467
+ const authorization = request.headers.get("authorization")?.trim();
468
+ if (authorization) {
469
+ const [scheme, token] = authorization.split(/\s+/, 2);
470
+ if (scheme?.toLowerCase() === "bearer" && token?.trim()) {
471
+ return token.trim();
472
+ }
473
+ }
474
+ const cookies = parseCookieHeader(request.headers.get("cookie"));
475
+ return cookies[sessionCookie] ?? null;
476
+ }
477
+ function getHoloSessionIdFromRequest(request) {
478
+ const cookieHeader = authRuntimeInternals.getRuntimeBindings().session.sessionCookie("");
479
+ const separator = cookieHeader.indexOf("=");
480
+ const cookieName = separator > 0 ? decodeURIComponent(cookieHeader.slice(0, separator)) : "";
481
+ if (!cookieName) {
482
+ return null;
483
+ }
484
+ const cookies = parseCookieHeader(request.headers.get("cookie"));
485
+ return cookies[cookieName] ?? null;
486
+ }
487
+ async function reuseExistingHoloSession(request, authenticated) {
488
+ const bindings = authRuntimeInternals.getRuntimeBindings();
489
+ const sessionId = getHoloSessionIdFromRequest(request);
490
+ if (!sessionId) {
491
+ return null;
492
+ }
493
+ bindings.context.setSessionId(authenticated.guard, sessionId);
494
+ const record = await bindings.session.read(sessionId);
495
+ const payload = authRuntimeInternals.readSessionPayload(record, authenticated.guard);
496
+ if (!payload || payload.guard !== authenticated.guard || payload.provider !== authenticated.authProvider || String(payload.userId) !== String(authenticated.user.id)) {
497
+ return null;
498
+ }
499
+ bindings.context.setCachedUser(authenticated.guard, authenticated.user);
500
+ return Object.freeze({
501
+ guard: authenticated.guard,
502
+ user: authenticated.user,
503
+ sessionId,
504
+ cookies: Object.freeze([])
505
+ });
506
+ }
507
+ async function verifySession(token, provider) {
508
+ const providerConfig = getConfiguredProviderConfig(provider);
509
+ const runtime = getProviderRuntime(providerConfig.name);
510
+ if (!runtime.verifySession) {
511
+ throw new Error(`[@holo-js/auth-clerk] Clerk provider runtime "${providerConfig.name}" does not implement verifySession().`);
512
+ }
513
+ return runtime.verifySession({
514
+ provider: providerConfig.name,
515
+ token,
516
+ config: providerConfig
517
+ });
518
+ }
519
+ async function verifyRequest(request, provider) {
520
+ const providerConfig = getConfiguredProviderConfig(provider);
521
+ const configuredRuntime = getBindings().providers[providerConfig.name];
522
+ const runtime = configuredRuntime ?? getProviderRuntime(providerConfig.name);
523
+ if (runtime.verifyRequest) {
524
+ return runtime.verifyRequest({
525
+ provider: providerConfig.name,
526
+ request,
527
+ config: providerConfig
528
+ });
529
+ }
530
+ const token = getSessionTokenFromRequest(request, providerConfig.sessionCookie);
531
+ if (!token) {
532
+ return null;
533
+ }
534
+ if (configuredRuntime) {
535
+ if (!configuredRuntime.verifySession) {
536
+ throw new Error(`[@holo-js/auth-clerk] Clerk provider runtime "${providerConfig.name}" does not implement verifySession().`);
537
+ }
538
+ return configuredRuntime.verifySession({
539
+ provider: providerConfig.name,
540
+ token,
541
+ config: providerConfig
542
+ });
543
+ }
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
+ });
570
+ }
571
+ async function syncIdentity(session, provider) {
572
+ const providerConfig = getConfiguredProviderConfig(provider);
573
+ const providerName = providerConfig.name;
574
+ const profile = session.user;
575
+ const { guard, authProvider, adapter } = resolveGuardAndProvider(providerName);
576
+ const verificationRequired = isEmailVerificationRequired();
577
+ const resolvedEmail = resolvePrimaryEmail(profile);
578
+ const verifiedEmail = resolvedEmail.emailVerified ? resolvedEmail.email : void 0;
579
+ if (verificationRequired && !verifiedEmail) {
580
+ throw new Error(`[@holo-js/auth-clerk] Clerk identity "${profile.id}" must provide a verified email address.`);
581
+ }
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
+ }
592
+ 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.");
600
+ }
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({
609
+ provider: providerName,
610
+ guard,
611
+ authProvider,
612
+ userId: requireUserId(
613
+ adapter,
614
+ relinkedUser,
615
+ "[@holo-js/auth-clerk] Relinked local users must expose a serializable id."
616
+ ),
617
+ profile,
618
+ previous: existingIdentity
619
+ });
620
+ await identityStore.save(identity3);
621
+ return Object.freeze({
622
+ provider: providerName,
623
+ guard,
624
+ authProvider,
625
+ status: "relinked",
626
+ user: serializeLocalUser(adapter, relinkedUser, authProvider),
627
+ identity: identity3,
628
+ session
629
+ });
630
+ }
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
+ )
640
+ );
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({
648
+ provider: providerName,
649
+ guard,
650
+ authProvider,
651
+ userId: requireUserId(
652
+ 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."
687
+ ),
688
+ profile
689
+ });
690
+ await identityStore.save(identity2);
691
+ return Object.freeze({
692
+ provider: providerName,
693
+ guard,
694
+ authProvider,
695
+ status: "linked",
696
+ user: serializeLocalUser(adapter, linked.user, authProvider),
697
+ identity: identity2,
698
+ session
699
+ });
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
+ });
729
+ }
730
+ async function authenticate(request, provider) {
731
+ const session = await verifyRequest(request, provider);
732
+ if (!session) {
733
+ return null;
734
+ }
735
+ const authenticated = await syncIdentity(session, provider);
736
+ const authSession = await reuseExistingHoloSession(request, authenticated) ?? await authRuntimeInternals.establishSessionForUser(authenticated.user, {
737
+ guard: authenticated.guard,
738
+ provider: authenticated.authProvider
739
+ });
740
+ return Object.freeze({
741
+ ...authenticated,
742
+ authSession
743
+ });
744
+ }
745
+ function configureClerkAuthRuntime(bindings) {
746
+ if (!bindings) {
747
+ clerkBindings = void 0;
748
+ return;
749
+ }
750
+ clerkBindings = Object.freeze({
751
+ providers: bindings.providers ?? clerkBindings?.providers,
752
+ identityStore: bindings.identityStore ?? clerkBindings?.identityStore
753
+ });
754
+ }
755
+ function resetClerkAuthRuntime() {
756
+ clerkBindings = void 0;
757
+ }
758
+ var clerkAuth = Object.freeze({
759
+ authenticate,
760
+ syncIdentity,
761
+ verifyRequest,
762
+ verifySession
763
+ });
764
+ var clerkAuthInternals = {
765
+ getBindings,
766
+ getConfiguredProviderConfig,
767
+ getSessionTokenFromRequest,
768
+ normalizeHostedProfile,
769
+ resolveConfiguredProviderName,
770
+ resolveDisplayName,
771
+ resolveEmailForCreation,
772
+ resolveGuardAndProvider,
773
+ resolvePrimaryEmail
774
+ };
775
+ export {
776
+ ClerkAuthConflictError,
777
+ authenticate,
778
+ clerkAuth,
779
+ clerkAuthInternals,
780
+ configureClerkAuthRuntime,
781
+ resetClerkAuthRuntime,
782
+ syncIdentity,
783
+ verifyRequest,
784
+ verifySession
785
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@holo-js/auth-clerk",
3
+ "version": "0.1.3",
4
+ "description": "Holo-JS Framework - Clerk auth integration contracts",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.mjs",
11
+ "default": "./dist/index.mjs"
12
+ }
13
+ },
14
+ "main": "./dist/index.mjs",
15
+ "types": "./dist/index.d.ts",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "stub": "tsup",
22
+ "typecheck": "tsc -p tsconfig.json --noEmit",
23
+ "test": "vitest --run"
24
+ },
25
+ "dependencies": {
26
+ "@holo-js/auth": "workspace:*",
27
+ "@holo-js/config": "workspace:*",
28
+ "@holo-js/session": "workspace:*"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.10.2",
32
+ "tsup": "^8.3.5",
33
+ "typescript": "^5.7.2",
34
+ "vitest": "^2.1.8"
35
+ }
36
+ }