@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.
- package/dist/index.d.ts +123 -26
- package/dist/index.mjs +750 -223
- package/package.json +6 -5
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
|
174
|
-
emailVerified
|
|
175
|
-
firstName
|
|
176
|
-
lastName
|
|
177
|
-
name:
|
|
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
|
|
180
|
-
emailAddresses
|
|
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
|
|
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 ||
|
|
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 =
|
|
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,
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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 =
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
615
|
-
"[@holo-js/auth-clerk]
|
|
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(
|
|
1028
|
+
await identityStore.save(identity2);
|
|
621
1029
|
return Object.freeze({
|
|
622
1030
|
provider: providerName,
|
|
623
1031
|
guard,
|
|
624
1032
|
authProvider,
|
|
625
|
-
status: "
|
|
626
|
-
user: serializeLocalUser(adapter,
|
|
627
|
-
identity:
|
|
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
|
|
632
|
-
|
|
633
|
-
authProvider,
|
|
634
|
-
profile,
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
|
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
|
-
|
|
654
|
-
"[@holo-js/auth-clerk]
|
|
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
|
|
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:
|
|
696
|
-
user: serializeLocalUser(adapter,
|
|
697
|
-
identity:
|
|
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(
|
|
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
|
-
|
|
1264
|
+
runtimeState.bindings = void 0;
|
|
748
1265
|
return;
|
|
749
1266
|
}
|
|
750
|
-
|
|
751
|
-
providers: bindings.providers ??
|
|
752
|
-
identityStore: bindings.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
|
-
|
|
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,
|