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