@better-auth/electron 1.7.0-beta.1 → 1.7.0-beta.10

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.
@@ -35,13 +35,6 @@ interface ElectronOptions extends ElectronSharedOptions {
35
35
  * @default "better-auth"
36
36
  */
37
37
  cookiePrefix?: string | undefined;
38
- /**
39
- * Override the origin for Electron API routes.
40
- * Enable this if you're facing cors origin issues with Electron API routes.
41
- *
42
- * @default false
43
- */
44
- disableOriginOverride?: boolean | undefined;
45
38
  }
46
39
  //#endregion
47
40
  //#region src/preload.d.ts
@@ -248,6 +241,7 @@ declare const requestAuthOptionsSchema: z.ZodObject<{
248
241
  disableRedirect: z.ZodOptional<z.ZodBoolean>;
249
242
  scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
250
243
  requestSignUp: z.ZodOptional<z.ZodBoolean>;
244
+ additionalParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
251
245
  additionalData: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
252
246
  }, z.core.$strip>;
253
247
  type ElectronRequestAuthOptions = z.infer<typeof requestAuthOptionsSchema>;
@@ -323,11 +317,6 @@ declare const electronClient: <O extends ElectronClientOptions>(options: O) => {
323
317
  mode?: RequestMode | undefined;
324
318
  cache?: RequestCache | undefined;
325
319
  credentials?: RequestCredentials | undefined;
326
- headers?: (HeadersInit & (HeadersInit | {
327
- accept: "application/json" | "text/plain" | "application/octet-stream";
328
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
329
- authorization: "Bearer" | "Basic";
330
- })) | undefined;
331
320
  integrity?: string | undefined;
332
321
  keepalive?: boolean | undefined;
333
322
  method?: string | undefined;
@@ -361,6 +350,12 @@ declare const electronClient: <O extends ElectronClientOptions>(options: O) => {
361
350
  prefix: string | (() => string | undefined) | undefined;
362
351
  value: string | (() => string | undefined) | undefined;
363
352
  }) | undefined;
353
+ headers?: {} | {
354
+ [x: string]: string | undefined;
355
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
356
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "multipart/form-data" | "application/octet-stream") | undefined;
357
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
358
+ } | undefined;
364
359
  body?: any;
365
360
  query?: any;
366
361
  params?: any;
@@ -378,11 +373,6 @@ declare const electronClient: <O extends ElectronClientOptions>(options: O) => {
378
373
  mode?: RequestMode | undefined;
379
374
  cache?: RequestCache | undefined;
380
375
  credentials?: RequestCredentials | undefined;
381
- headers?: (HeadersInit & (HeadersInit | {
382
- accept: "application/json" | "text/plain" | "application/octet-stream";
383
- "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
384
- authorization: "Bearer" | "Basic";
385
- })) | undefined;
386
376
  integrity?: string | undefined;
387
377
  keepalive?: boolean | undefined;
388
378
  method?: string | undefined;
@@ -416,6 +406,12 @@ declare const electronClient: <O extends ElectronClientOptions>(options: O) => {
416
406
  prefix: string | (() => string | undefined) | undefined;
417
407
  value: string | (() => string | undefined) | undefined;
418
408
  }) | undefined;
409
+ headers?: {} | {
410
+ [x: string]: string | undefined;
411
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
412
+ "content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "multipart/form-data" | "application/octet-stream") | undefined;
413
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
414
+ } | undefined;
419
415
  body?: any;
420
416
  query?: any;
421
417
  params?: any;
package/dist/client.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as setupRenderer, a as authenticate, c as ElectronClientOptions, d as Storage, f as FetchUserImageResult, g as SetupRendererConfig, h as ExposedBridges, i as ElectronRequestAuthOptions, l as ElectronProxyClientOptions, m as normalizeUserOutput, n as handleDeepLink, o as kElectron, p as fetchUserImage, r as ElectronAuthenticateOptions, s as requestAuth, t as electronClient, u as ElectronSharedClientOptions, y as ElectronSharedOptions } from "./client-D6xRwPM0.mjs";
1
+ import { _ as setupRenderer, a as authenticate, c as ElectronClientOptions, d as Storage, f as FetchUserImageResult, g as SetupRendererConfig, h as ExposedBridges, i as ElectronRequestAuthOptions, l as ElectronProxyClientOptions, m as normalizeUserOutput, n as handleDeepLink, o as kElectron, p as fetchUserImage, r as ElectronAuthenticateOptions, s as requestAuth, t as electronClient, u as ElectronSharedClientOptions, y as ElectronSharedOptions } from "./client-D5QKMJbp.mjs";
2
2
  export { ElectronAuthenticateOptions, ElectronClientOptions, ElectronProxyClientOptions, ElectronRequestAuthOptions, ElectronSharedClientOptions, ElectronSharedOptions, ExposedBridges, FetchUserImageResult, SetupRendererConfig, Storage, authenticate, electronClient, fetchUserImage, handleDeepLink, kElectron, normalizeUserOutput, requestAuth, setupRenderer };
package/dist/client.mjs CHANGED
@@ -1,16 +1,17 @@
1
- import { t as PACKAGE_VERSION } from "./version-C22JHwcK.mjs";
1
+ import { t as PACKAGE_VERSION } from "./version-C-W6yUfI.mjs";
2
2
  import { n as isProcessType, r as parseProtocolScheme, t as getChannelPrefixWithDelimiter } from "./utils-DxDKRT6e.mjs";
3
3
  import { BetterAuthError } from "@better-auth/core/error";
4
4
  import { base64, base64Url } from "@better-auth/utils/base64";
5
5
  import { APIError as APIError$1, getBaseURL, isDevelopment, isTest, safeJSONParse } from "better-auth";
6
6
  import { generateRandomString } from "better-auth/crypto";
7
7
  import * as z from "zod";
8
- import { Buffer } from "node:buffer";
9
- import { createHash } from "@better-auth/utils/hash";
10
8
  import { signInSocial } from "better-auth/api";
11
- import { parseSetCookieHeader } from "better-auth/cookies";
9
+ import { cookieNameRegex, parseSetCookieHeader } from "better-auth/cookies";
10
+ import { generateCodeChallenge } from "better-auth/oauth2";
11
+ import { Buffer } from "node:buffer";
12
12
  import electron, { shell } from "electron";
13
13
  import { isDevelopment as isDevelopment$1 } from "@better-auth/core/env";
14
+ import { isPublicRoutableHost } from "@better-auth/core/utils/host";
14
15
  import { resolve } from "node:path";
15
16
  //#region src/user.ts
16
17
  const { net } = electron;
@@ -37,7 +38,7 @@ async function fetchUserImage(baseURL, url, options) {
37
38
  parsed = new URL(relative, base);
38
39
  }
39
40
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
40
- if (!isDevelopment$1() && isLocalOrigin(parsed)) return null;
41
+ if (!isDevelopment$1() && !isPublicRoutableHost(parsed.hostname)) return null;
41
42
  resolvedUrl = parsed.href;
42
43
  } catch {
43
44
  return null;
@@ -116,25 +117,6 @@ async function decodeDataImageUrl(url, options) {
116
117
  return null;
117
118
  }
118
119
  }
119
- function isLocalOrigin(parsed) {
120
- const hostname = parsed.hostname.toLowerCase();
121
- if (hostname === "localhost") return true;
122
- const m = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
123
- if (m) {
124
- const a = Number(m[1]);
125
- const b = Number(m[2]);
126
- if (a === 127) return true;
127
- if (a === 10) return true;
128
- if (a === 172 && b >= 16 && b <= 31) return true;
129
- if (a === 192 && b === 168) return true;
130
- if (a === 169 && b === 254) return true;
131
- return false;
132
- }
133
- const h = hostname.replace(/^\[|\]$/g, "");
134
- if (h === "::1" || /^(0:){7}1$/.test(h) || h === "0:0:0:0:0:0:0:1") return true;
135
- if (/^fe[89ab][0-9a-f]/i.test(h)) return true;
136
- return false;
137
- }
138
120
  function detectImageType(bytes) {
139
121
  if (bytes.length < 12) return null;
140
122
  if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10) return "image/png";
@@ -168,8 +150,8 @@ async function requestAuth(clientOptions, options, cfg) {
168
150
  if (!isProcessType("browser")) throw new BetterAuthError("`requestAuth` can only be called in the main process");
169
151
  const { randomBytes } = await import("node:crypto");
170
152
  const state = generateRandomString(16, "A-Z", "a-z", "0-9");
171
- const codeVerifier = base64Url.encode(randomBytes(32));
172
- const codeChallenge = base64Url.encode(await createHash("SHA-256").digest(codeVerifier));
153
+ const codeVerifier = base64Url.encode(randomBytes(32), { padding: false });
154
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
173
155
  (globalThis[kElectron] ??= /* @__PURE__ */ new Map()).set(state, codeVerifier);
174
156
  let url = null;
175
157
  if (cfg?.provider) {
@@ -186,7 +168,6 @@ async function requestAuth(clientOptions, options, cfg) {
186
168
  } else url = new URL(options.signInURL);
187
169
  url.searchParams.set("client_id", options.clientID || "electron");
188
170
  url.searchParams.set("code_challenge", codeChallenge);
189
- url.searchParams.set("code_challenge_method", "S256");
190
171
  url.searchParams.set("state", state);
191
172
  await shell.openExternal(url.toString(), { activate: true });
192
173
  }
@@ -492,10 +473,13 @@ function getCookie(cookie) {
492
473
  try {
493
474
  parsed = JSON.parse(cookie);
494
475
  } catch (_e) {}
495
- return Object.entries(parsed).reduce((acc, [key, value]) => {
496
- if (value.expires && new Date(value.expires) < /* @__PURE__ */ new Date()) return acc;
497
- return `${acc}; ${key}=${value.value}`;
498
- }, "");
476
+ const pairs = [];
477
+ for (const [key, value] of Object.entries(parsed)) {
478
+ if (value.expires && new Date(value.expires) < /* @__PURE__ */ new Date()) continue;
479
+ if (!cookieNameRegex.test(key)) continue;
480
+ pairs.push(`${key}=${encodeURIComponent(value.value)}`);
481
+ }
482
+ return pairs.join("; ");
499
483
  }
500
484
  /**
501
485
  * Compare if session cookies have actually changed by comparing their values.
@@ -594,6 +578,16 @@ const electronClient = (options) => {
594
578
  const cookieName = `${opts.storagePrefix}.cookie`;
595
579
  const localCacheName = `${opts.storagePrefix}.local_cache`;
596
580
  const { getDecrypted, setEncrypted } = storageAdapter(opts.storage, new Set([cookieName, localCacheName]));
581
+ const clearSessionCache = () => {
582
+ setEncrypted(cookieName, "{}");
583
+ store?.atoms.session?.set({
584
+ ...store.atoms.session.get(),
585
+ data: null,
586
+ error: null,
587
+ isPending: false
588
+ });
589
+ setEncrypted(localCacheName, "{}");
590
+ };
597
591
  if ((isDevelopment() || isTest()) && /^(?!\.)(?!.*\.\.)(?!.*\.$)[^.]+\.[^.]+$/.test(scheme)) console.warn("The provided scheme does not follow the reverse domain name notation. For example: `app.example.com` -> `com.example.app`.");
598
592
  return {
599
593
  id: "electron",
@@ -607,22 +601,13 @@ const electronClient = (options) => {
607
601
  options ||= {};
608
602
  options.credentials = "omit";
609
603
  options.headers = {
604
+ origin: `${scheme}:/`,
610
605
  ...options.headers,
611
606
  cookie,
612
607
  "user-agent": app.userAgentFallback,
613
- "electron-origin": `${scheme}:/`,
614
608
  "x-skip-oauth-proxy": "true"
615
609
  };
616
- if (url.endsWith("/sign-out")) {
617
- setEncrypted(cookieName, "{}");
618
- store?.atoms.session?.set({
619
- ...store.atoms.session.get(),
620
- data: null,
621
- error: null,
622
- isPending: false
623
- });
624
- setEncrypted(localCacheName, "{}");
625
- }
610
+ if (url.endsWith("/sign-out")) clearSessionCache();
626
611
  return {
627
612
  url,
628
613
  options
@@ -645,6 +630,7 @@ const electronClient = (options) => {
645
630
  const data = context.data;
646
631
  setEncrypted(localCacheName, JSON.stringify(data));
647
632
  }
633
+ if (context.request.url.toString().includes("/sign-out")) clearSessionCache();
648
634
  },
649
635
  onError: async (context) => {
650
636
  webContents.getFocusedWebContents()?.send(`${getChannelPrefixWithDelimiter(opts.channelPrefix)}error`, {
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { f as FetchUserImageResult, m as normalizeUserOutput, p as fetchUserImage, v as ElectronOptions, y as ElectronSharedOptions } from "./client-D6xRwPM0.mjs";
1
+ import { f as FetchUserImageResult, m as normalizeUserOutput, p as fetchUserImage, v as ElectronOptions, y as ElectronSharedOptions } from "./client-D5QKMJbp.mjs";
2
2
  import * as better_auth0 from "better-auth";
3
3
  import * as z from "zod";
4
4
  import * as _better_auth_core0 from "@better-auth/core";
@@ -16,9 +16,6 @@ declare module "@better-auth/core" {
16
16
  declare const electron: (options?: ElectronOptions | undefined) => {
17
17
  id: "electron";
18
18
  version: string;
19
- onRequest(request: Request, _ctx: better_auth0.AuthContext): Promise<{
20
- request: Request;
21
- } | undefined>;
22
19
  hooks: {
23
20
  after: ({
24
21
  matcher: (ctx: HookEndpointContext) => boolean;
@@ -205,7 +202,6 @@ declare const electron: (options?: ElectronOptions | undefined) => {
205
202
  provider: z.ZodString;
206
203
  state: z.ZodString;
207
204
  code_challenge: z.ZodString;
208
- code_challenge_method: z.ZodOptional<z.ZodString>;
209
205
  }, z.core.$strip>;
210
206
  metadata: {
211
207
  scope: "http";
@@ -255,7 +251,6 @@ declare const electron: (options?: ElectronOptions | undefined) => {
255
251
  client_id: z.ZodString;
256
252
  state: z.ZodString;
257
253
  code_challenge: z.ZodString;
258
- code_challenge_method: z.ZodOptional<z.ZodString>;
259
254
  }, z.core.$strip>;
260
255
  body: z.ZodObject<{
261
256
  callbackURL: z.ZodOptional<z.ZodString>;
@@ -325,7 +320,6 @@ declare const electron: (options?: ElectronOptions | undefined) => {
325
320
  codeExpiresIn: number;
326
321
  redirectCookieExpiresIn: number;
327
322
  cookiePrefix: string;
328
- disableOriginOverride?: boolean | undefined;
329
323
  clientID: string;
330
324
  };
331
325
  $ERROR_CODES: {
package/dist/index.mjs CHANGED
@@ -1,18 +1,16 @@
1
- import { t as PACKAGE_VERSION } from "./version-C22JHwcK.mjs";
1
+ import { t as PACKAGE_VERSION } from "./version-C-W6yUfI.mjs";
2
2
  import { createAuthMiddleware } from "@better-auth/core/api";
3
3
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
4
4
  import { base64Url } from "@better-auth/utils/base64";
5
5
  import { defineErrorCodes, safeJSONParse } from "better-auth";
6
6
  import { generateRandomString } from "better-auth/crypto";
7
7
  import * as z from "zod";
8
- import { Buffer } from "node:buffer";
9
- import { timingSafeEqual } from "node:crypto";
10
8
  import { safeJSONParse as safeJSONParse$1 } from "@better-auth/core/utils/json";
11
- import { createHash } from "@better-auth/utils/hash";
12
9
  import { betterFetch } from "@better-fetch/fetch";
13
10
  import { createAuthEndpoint, sessionMiddleware } from "better-auth/api";
14
11
  import { setSessionCookie } from "better-auth/cookies";
15
12
  import { parseUserOutput } from "better-auth/db";
13
+ import { generateCodeChallenge } from "better-auth/oauth2";
16
14
  //#region src/error-codes.ts
17
15
  const ELECTRON_ERROR_CODES = defineErrorCodes({
18
16
  INVALID_CLIENT_ID: "Invalid client ID",
@@ -55,18 +53,13 @@ const electronToken = (_opts) => createAuthEndpoint("/electron/token", {
55
53
  }
56
54
  }
57
55
  }, async (ctx) => {
58
- const token = await ctx.context.internalAdapter.findVerificationValue(`electron:${ctx.body.token}`);
59
- if (!token || token.expiresAt < /* @__PURE__ */ new Date()) throw APIError.from("NOT_FOUND", ELECTRON_ERROR_CODES.INVALID_TOKEN);
56
+ const token = await ctx.context.internalAdapter.consumeVerificationValue(`electron:${ctx.body.token}`);
57
+ if (!token) throw APIError.from("NOT_FOUND", ELECTRON_ERROR_CODES.INVALID_TOKEN);
60
58
  const tokenRecord = safeJSONParse$1(token.value);
61
59
  if (!tokenRecord) throw APIError.from("INTERNAL_SERVER_ERROR", ELECTRON_ERROR_CODES.INVALID_TOKEN);
62
60
  if (tokenRecord.state !== ctx.body.state) throw APIError.from("BAD_REQUEST", ELECTRON_ERROR_CODES.STATE_MISMATCH);
63
61
  if (!tokenRecord.codeChallenge) throw APIError.from("BAD_REQUEST", ELECTRON_ERROR_CODES.MISSING_CODE_CHALLENGE);
64
- if (tokenRecord.codeChallengeMethod === "s256") {
65
- const codeChallenge = Buffer.from(base64Url.decode(tokenRecord.codeChallenge));
66
- const codeVerifier = Buffer.from(await createHash("SHA-256").digest(ctx.body.code_verifier));
67
- if (codeChallenge.length !== codeVerifier.length || !timingSafeEqual(codeChallenge, codeVerifier)) throw APIError.from("BAD_REQUEST", ELECTRON_ERROR_CODES.INVALID_CODE_VERIFIER);
68
- } else if (tokenRecord.codeChallenge !== ctx.body.code_verifier) throw APIError.from("BAD_REQUEST", ELECTRON_ERROR_CODES.INVALID_CODE_VERIFIER);
69
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(`electron:${ctx.body.token}`);
62
+ if (await generateCodeChallenge(ctx.body.code_verifier) !== tokenRecord.codeChallenge) throw APIError.from("BAD_REQUEST", ELECTRON_ERROR_CODES.INVALID_CODE_VERIFIER);
70
63
  const user = await ctx.context.internalAdapter.findUserById(tokenRecord.userId);
71
64
  if (!user) throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.USER_NOT_FOUND);
72
65
  const session = await ctx.context.internalAdapter.createSession(user.id);
@@ -83,8 +76,7 @@ const electronToken = (_opts) => createAuthEndpoint("/electron/token", {
83
76
  const electronInitOAuthProxyQuerySchema = z.object({
84
77
  provider: z.string().nonempty(),
85
78
  state: z.string(),
86
- code_challenge: z.string(),
87
- code_challenge_method: z.string().optional()
79
+ code_challenge: z.string()
88
80
  });
89
81
  const electronInitOAuthProxy = (opts) => createAuthEndpoint("/electron/init-oauth-proxy", {
90
82
  method: "GET",
@@ -122,7 +114,6 @@ const electronInitOAuthProxy = (opts) => createAuthEndpoint("/electron/init-oaut
122
114
  const searchParams = new URLSearchParams();
123
115
  searchParams.set("client_id", opts.clientID || "electron");
124
116
  searchParams.set("code_challenge", ctx.query.code_challenge);
125
- searchParams.set("code_challenge_method", ctx.query.code_challenge_method || "plain");
126
117
  searchParams.set("state", ctx.query.state);
127
118
  const res = await betterFetch(`/sign-in/social?${searchParams.toString()}`, {
128
119
  baseURL: ctx.context.baseURL,
@@ -145,8 +136,7 @@ const electronInitOAuthProxy = (opts) => createAuthEndpoint("/electron/init-oaut
145
136
  const electronTransferUserQuerySchema = z.object({
146
137
  client_id: z.string(),
147
138
  state: z.string(),
148
- code_challenge: z.string(),
149
- code_challenge_method: z.string().optional()
139
+ code_challenge: z.string()
150
140
  });
151
141
  const electronTransferUserBodySchema = z.object({ callbackURL: z.string().optional() });
152
142
  const electronTransferUser = (_opts, { handleTransfer }) => createAuthEndpoint("/electron/transfer-user", {
@@ -201,7 +191,7 @@ const electron = (options) => {
201
191
  return !!(ctx.path?.startsWith("/sign-in") || ctx.path?.startsWith("/sign-up") || ctx.path?.startsWith("/callback") || ctx.path?.startsWith("/magic-link/verify") || ctx.path?.startsWith("/email-otp/verify-email") || ctx.path?.startsWith("/verify-email") || ctx.path?.startsWith("/one-tap/callback") || ctx.path?.startsWith("/passkey/verify-authentication") || ctx.path?.startsWith("/phone-number/verify"));
202
192
  };
203
193
  const handleTransfer = async (ctx, payload) => {
204
- const { client_id, state, code_challenge, code_challenge_method = "plain" } = payload;
194
+ const { client_id, state, code_challenge } = payload;
205
195
  const userId = ctx.context.session?.user.id || ctx.context.newSession?.user.id;
206
196
  if (!userId || client_id !== opts.clientID) return null;
207
197
  if (!state) throw APIError.from("BAD_REQUEST", ELECTRON_ERROR_CODES.MISSING_STATE);
@@ -215,7 +205,6 @@ const electron = (options) => {
215
205
  value: JSON.stringify({
216
206
  userId,
217
207
  codeChallenge: code_challenge,
218
- codeChallengeMethod: code_challenge_method.toLowerCase(),
219
208
  state
220
209
  }),
221
210
  expiresAt
@@ -234,14 +223,6 @@ const electron = (options) => {
234
223
  return {
235
224
  id: "electron",
236
225
  version: PACKAGE_VERSION,
237
- async onRequest(request, _ctx) {
238
- if (opts.disableOriginOverride || request.headers.get("origin")) return;
239
- const electronOrigin = request.headers.get("electron-origin");
240
- if (!electronOrigin) return;
241
- const req = request.clone();
242
- req.headers.set("origin", electronOrigin);
243
- return { request: req };
244
- },
245
226
  hooks: { after: [{
246
227
  matcher: (ctx) => !hookMatcher(ctx),
247
228
  handler: createAuthMiddleware(async (ctx) => {
@@ -256,7 +237,6 @@ const electron = (options) => {
256
237
  const querySchema = z.object({
257
238
  client_id: z.string(),
258
239
  code_challenge: z.string().nonempty(),
259
- code_challenge_method: z.string().optional().default("plain"),
260
240
  state: z.string().nonempty()
261
241
  });
262
242
  const cookie = ctx.context.createAuthCookie("transfer_token", { maxAge: opts.codeExpiresIn });