@spfn/auth 0.3.0-beta.16 → 0.3.0-beta.18

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.
@@ -25,4 +25,31 @@ interface OAuthCallbackProps {
25
25
  }
26
26
  declare function OAuthCallback({ apiBasePath, loadingComponent, errorComponent, onSuccess, onError, }: OAuthCallbackProps): react_jsx_runtime.JSX.Element | null;
27
27
 
28
- export { OAuthCallback, type OAuthCallbackProps };
28
+ /**
29
+ * @spfn/auth - Return-path validation
30
+ *
31
+ * One rule for every flow that hands a caller-supplied destination back to the
32
+ * browser: the verified-email signup link, the password reset link, and the
33
+ * OAuth start/callback seams. Apps that build their own destination before
34
+ * calling an auth route import the same function rather than writing a second
35
+ * rule that drifts from this one.
36
+ *
37
+ * The module imports nothing on purpose — it is part of the client bundle
38
+ * (`@spfn/auth/nextjs/client`), which must not pull server code in behind it.
39
+ */
40
+ /**
41
+ * Whether a return path can be handed back to the browser.
42
+ *
43
+ * Only a path within the app is allowed. The rejected shapes are the ones that
44
+ * turn a return path into an open redirect: an absolute URL, a protocol-relative
45
+ * `//host` that a browser reads as another origin, a backslash that some
46
+ * browsers normalize into a slash, any `..` traversal, and any character a URL
47
+ * parser strips before parsing (see above).
48
+ *
49
+ * The value is judged exactly as written: nothing is percent-decoded here. A
50
+ * `/a%0d%0a` is therefore a path containing those six literal characters and is
51
+ * accepted — no decoder downstream turns it back into header bytes.
52
+ */
53
+ declare function isSafeReturnPath(returnPath: string): boolean;
54
+
55
+ export { OAuthCallback, type OAuthCallbackProps, isSafeReturnPath };
@@ -2,7 +2,27 @@
2
2
 
3
3
  // src/nextjs/components/oauth-callback.tsx
4
4
  import { useEffect, useState } from "react";
5
+
6
+ // src/lib/return-path.ts
7
+ var URL_STRIPPED_CHARACTER = /[\t\n\r]/;
8
+ function isSafeReturnPath(returnPath) {
9
+ if (!returnPath.startsWith("/")) {
10
+ return false;
11
+ }
12
+ if (returnPath.startsWith("//") || returnPath.includes("\\")) {
13
+ return false;
14
+ }
15
+ if (returnPath.includes("..") || URL_STRIPPED_CHARACTER.test(returnPath)) {
16
+ return false;
17
+ }
18
+ return !/^\/[^/?#]*:/.test(returnPath);
19
+ }
20
+
21
+ // src/nextjs/components/oauth-callback.tsx
5
22
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
23
+ function toSafePath(value) {
24
+ return value && isSafeReturnPath(value) ? value : "/";
25
+ }
6
26
  function OAuthCallback({
7
27
  apiBasePath = "/api/rpc",
8
28
  loadingComponent,
@@ -18,7 +38,7 @@ function OAuthCallback({
18
38
  const params = new URLSearchParams(window.location.search);
19
39
  const userId = params.get("userId");
20
40
  const keyId = params.get("keyId");
21
- const returnUrl = params.get("returnUrl") || "/";
41
+ const returnUrl = toSafePath(params.get("returnUrl"));
22
42
  const errorParam = params.get("error");
23
43
  if (errorParam) {
24
44
  throw new Error(errorParam);
@@ -46,7 +66,7 @@ function OAuthCallback({
46
66
  }
47
67
  const data = await response.json();
48
68
  onSuccess?.(userId);
49
- window.location.href = data.returnUrl || returnUrl;
69
+ window.location.href = toSafePath(data.returnUrl || returnUrl);
50
70
  } catch (err) {
51
71
  const message = err instanceof Error ? err.message : "OAuth failed";
52
72
  setError(message);
@@ -75,6 +95,7 @@ function OAuthCallback({
75
95
  return null;
76
96
  }
77
97
  export {
78
- OAuthCallback
98
+ OAuthCallback,
99
+ isSafeReturnPath
79
100
  };
80
101
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/nextjs/components/oauth-callback.tsx"],"sourcesContent":["'use client';\n\n/**\n * OAuthCallback Component\n *\n * OAuth 콜백 페이지용 클라이언트 컴포넌트\n * URL params에서 userId, keyId를 추출하여 oauthFinalize API 호출 후 returnUrl로 리다이렉트\n *\n * @example\n * ```tsx\n * // app/auth/callback/page.tsx\n * export { OAuthCallback as default } from '@spfn/auth/nextjs/client';\n * ```\n */\n\nimport { useEffect, useState } from 'react';\n\nexport interface OAuthCallbackProps\n{\n /**\n * API base path for RPC calls\n * @default '/api/rpc'\n */\n apiBasePath?: string;\n\n /**\n * Custom loading component\n */\n loadingComponent?: React.ReactNode;\n\n /**\n * Custom error component\n */\n errorComponent?: (error: string) => React.ReactNode;\n\n /**\n * Callback after successful OAuth\n */\n onSuccess?: (userId: string) => void;\n\n /**\n * Callback on error\n */\n onError?: (error: string) => void;\n}\n\nexport function OAuthCallback({\n apiBasePath = '/api/rpc',\n loadingComponent,\n errorComponent,\n onSuccess,\n onError,\n}: OAuthCallbackProps)\n{\n const [error, setError] = useState<string | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n\n useEffect(() =>\n {\n async function finalizeOAuth()\n {\n try\n {\n const params = new URLSearchParams(window.location.search);\n const userId = params.get('userId');\n const keyId = params.get('keyId');\n const returnUrl = params.get('returnUrl') || '/';\n const errorParam = params.get('error');\n\n // Handle error from backend\n if (errorParam)\n {\n throw new Error(errorParam);\n }\n\n if (!userId || !keyId)\n {\n throw new Error('Missing required parameters');\n }\n\n // Call oauthFinalize API\n const response = await fetch(`${apiBasePath}/oauthFinalize`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n credentials: 'include',\n body: JSON.stringify({\n body: {\n userId,\n keyId,\n returnUrl,\n },\n }),\n });\n\n if (!response.ok)\n {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.message || 'Failed to finalize OAuth');\n }\n\n const data = await response.json();\n\n onSuccess?.(userId);\n\n // Redirect to returnUrl\n window.location.href = data.returnUrl || returnUrl;\n }\n catch (err)\n {\n const message = err instanceof Error ? err.message : 'OAuth failed';\n setError(message);\n setIsLoading(false);\n onError?.(message);\n }\n }\n\n finalizeOAuth();\n }, [apiBasePath, onSuccess, onError]);\n\n if (error)\n {\n if (errorComponent)\n {\n return <>{errorComponent(error)}</>;\n }\n\n return (\n <div style={{ padding: '20px', textAlign: 'center' }}>\n <h2>Authentication Error</h2>\n <p style={{ color: 'red' }}>{error}</p>\n <button onClick={() => window.location.href = '/'}>\n Go Home\n </button>\n </div>\n );\n }\n\n if (isLoading)\n {\n if (loadingComponent)\n {\n return <>{loadingComponent}</>;\n }\n\n return (\n <div style={{ padding: '20px', textAlign: 'center' }}>\n <p>Completing authentication...</p>\n </div>\n );\n }\n\n return null;\n}\n\nexport default OAuthCallback;\n"],"mappings":";;;AAeA,SAAS,WAAW,gBAAgB;AA8GjB,wBAIP,YAJO;AA/EZ,SAAS,cAAc;AAAA,EAC1B,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ,GACA;AACI,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,IAAI;AAE/C,YAAU,MACV;AACI,mBAAe,gBACf;AACI,UACA;AACI,cAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,cAAM,SAAS,OAAO,IAAI,QAAQ;AAClC,cAAM,QAAQ,OAAO,IAAI,OAAO;AAChC,cAAM,YAAY,OAAO,IAAI,WAAW,KAAK;AAC7C,cAAM,aAAa,OAAO,IAAI,OAAO;AAGrC,YAAI,YACJ;AACI,gBAAM,IAAI,MAAM,UAAU;AAAA,QAC9B;AAEA,YAAI,CAAC,UAAU,CAAC,OAChB;AACI,gBAAM,IAAI,MAAM,6BAA6B;AAAA,QACjD;AAGA,cAAM,WAAW,MAAM,MAAM,GAAG,WAAW,kBAAkB;AAAA,UACzD,QAAQ;AAAA,UACR,SAAS;AAAA,YACL,gBAAgB;AAAA,UACpB;AAAA,UACA,aAAa;AAAA,UACb,MAAM,KAAK,UAAU;AAAA,YACjB,MAAM;AAAA,cACF;AAAA,cACA;AAAA,cACA;AAAA,YACJ;AAAA,UACJ,CAAC;AAAA,QACL,CAAC;AAED,YAAI,CAAC,SAAS,IACd;AACI,gBAAMA,QAAO,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACnD,gBAAM,IAAI,MAAMA,MAAK,WAAW,0BAA0B;AAAA,QAC9D;AAEA,cAAM,OAAO,MAAM,SAAS,KAAK;AAEjC,oBAAY,MAAM;AAGlB,eAAO,SAAS,OAAO,KAAK,aAAa;AAAA,MAC7C,SACO,KACP;AACI,cAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,iBAAS,OAAO;AAChB,qBAAa,KAAK;AAClB,kBAAU,OAAO;AAAA,MACrB;AAAA,IACJ;AAEA,kBAAc;AAAA,EAClB,GAAG,CAAC,aAAa,WAAW,OAAO,CAAC;AAEpC,MAAI,OACJ;AACI,QAAI,gBACJ;AACI,aAAO,gCAAG,yBAAe,KAAK,GAAE;AAAA,IACpC;AAEA,WACI,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,WAAW,SAAS,GAC/C;AAAA,0BAAC,QAAG,kCAAoB;AAAA,MACxB,oBAAC,OAAE,OAAO,EAAE,OAAO,MAAM,GAAI,iBAAM;AAAA,MACnC,oBAAC,YAAO,SAAS,MAAM,OAAO,SAAS,OAAO,KAAK,qBAEnD;AAAA,OACJ;AAAA,EAER;AAEA,MAAI,WACJ;AACI,QAAI,kBACJ;AACI,aAAO,gCAAG,4BAAiB;AAAA,IAC/B;AAEA,WACI,oBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,WAAW,SAAS,GAC/C,8BAAC,OAAE,0CAA4B,GACnC;AAAA,EAER;AAEA,SAAO;AACX;","names":["data"]}
1
+ {"version":3,"sources":["../../src/nextjs/components/oauth-callback.tsx","../../src/lib/return-path.ts"],"sourcesContent":["'use client';\n\n/**\n * OAuthCallback Component\n *\n * OAuth 콜백 페이지용 클라이언트 컴포넌트\n * URL params에서 userId, keyId를 추출하여 oauthFinalize API 호출 후 returnUrl로 리다이렉트\n *\n * @example\n * ```tsx\n * // app/auth/callback/page.tsx\n * export { OAuthCallback as default } from '@spfn/auth/nextjs/client';\n * ```\n */\n\nimport { useEffect, useState } from 'react';\n\nimport { isSafeReturnPath } from '../../lib/return-path';\n\n/**\n * The destination to navigate to, or `/` when the value would leave the app.\n *\n * Both the query parameter and the value echoed by `oauthFinalize` pass through\n * here. A callback URL is something a user can be handed, so neither is trusted\n * to be a path inside the app — without this an absolute URL in `?returnUrl=`\n * would make a genuine login end on someone else's origin.\n */\nfunction toSafePath(value: string | null | undefined): string\n{\n return value && isSafeReturnPath(value) ? value : '/';\n}\n\nexport interface OAuthCallbackProps\n{\n /**\n * API base path for RPC calls\n * @default '/api/rpc'\n */\n apiBasePath?: string;\n\n /**\n * Custom loading component\n */\n loadingComponent?: React.ReactNode;\n\n /**\n * Custom error component\n */\n errorComponent?: (error: string) => React.ReactNode;\n\n /**\n * Callback after successful OAuth\n */\n onSuccess?: (userId: string) => void;\n\n /**\n * Callback on error\n */\n onError?: (error: string) => void;\n}\n\nexport function OAuthCallback({\n apiBasePath = '/api/rpc',\n loadingComponent,\n errorComponent,\n onSuccess,\n onError,\n}: OAuthCallbackProps)\n{\n const [error, setError] = useState<string | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n\n useEffect(() =>\n {\n async function finalizeOAuth()\n {\n try\n {\n const params = new URLSearchParams(window.location.search);\n const userId = params.get('userId');\n const keyId = params.get('keyId');\n const returnUrl = toSafePath(params.get('returnUrl'));\n const errorParam = params.get('error');\n\n // Handle error from backend\n if (errorParam)\n {\n throw new Error(errorParam);\n }\n\n if (!userId || !keyId)\n {\n throw new Error('Missing required parameters');\n }\n\n // Call oauthFinalize API\n const response = await fetch(`${apiBasePath}/oauthFinalize`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n credentials: 'include',\n body: JSON.stringify({\n body: {\n userId,\n keyId,\n returnUrl,\n },\n }),\n });\n\n if (!response.ok)\n {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.message || 'Failed to finalize OAuth');\n }\n\n const data = await response.json();\n\n onSuccess?.(userId);\n\n // Redirect to returnUrl\n window.location.href = toSafePath(data.returnUrl || returnUrl);\n }\n catch (err)\n {\n const message = err instanceof Error ? err.message : 'OAuth failed';\n setError(message);\n setIsLoading(false);\n onError?.(message);\n }\n }\n\n finalizeOAuth();\n }, [apiBasePath, onSuccess, onError]);\n\n if (error)\n {\n if (errorComponent)\n {\n return <>{errorComponent(error)}</>;\n }\n\n return (\n <div style={{ padding: '20px', textAlign: 'center' }}>\n <h2>Authentication Error</h2>\n <p style={{ color: 'red' }}>{error}</p>\n <button onClick={() => window.location.href = '/'}>\n Go Home\n </button>\n </div>\n );\n }\n\n if (isLoading)\n {\n if (loadingComponent)\n {\n return <>{loadingComponent}</>;\n }\n\n return (\n <div style={{ padding: '20px', textAlign: 'center' }}>\n <p>Completing authentication...</p>\n </div>\n );\n }\n\n return null;\n}\n\nexport default OAuthCallback;\n","/**\n * @spfn/auth - Return-path validation\n *\n * One rule for every flow that hands a caller-supplied destination back to the\n * browser: the verified-email signup link, the password reset link, and the\n * OAuth start/callback seams. Apps that build their own destination before\n * calling an auth route import the same function rather than writing a second\n * rule that drifts from this one.\n *\n * The module imports nothing on purpose — it is part of the client bundle\n * (`@spfn/auth/nextjs/client`), which must not pull server code in behind it.\n */\n\n/**\n * The characters a URL parser deletes from anywhere in its input before it reads\n * the input as a URL: ASCII tab, LF and CR (WHATWG URL, \"remove all ASCII tab or\n * newline\"). The rule below reads the value as written, so a value holding one of\n * them is not the value the browser parses — `/<tab>/evil.com` is read as the\n * protocol-relative `//evil.com` and lands on another origin. Refusing the three\n * outright also keeps a raw CR or LF out of any `Location` header the value\n * reaches, which is what would split that header in two.\n */\nconst URL_STRIPPED_CHARACTER = /[\\t\\n\\r]/;\n\n/**\n * Whether a return path can be handed back to the browser.\n *\n * Only a path within the app is allowed. The rejected shapes are the ones that\n * turn a return path into an open redirect: an absolute URL, a protocol-relative\n * `//host` that a browser reads as another origin, a backslash that some\n * browsers normalize into a slash, any `..` traversal, and any character a URL\n * parser strips before parsing (see above).\n *\n * The value is judged exactly as written: nothing is percent-decoded here. A\n * `/a%0d%0a` is therefore a path containing those six literal characters and is\n * accepted — no decoder downstream turns it back into header bytes.\n */\nexport function isSafeReturnPath(returnPath: string): boolean\n{\n if (!returnPath.startsWith('/'))\n {\n return false;\n }\n\n if (returnPath.startsWith('//') || returnPath.includes('\\\\'))\n {\n return false;\n }\n\n if (returnPath.includes('..') || URL_STRIPPED_CHARACTER.test(returnPath))\n {\n return false;\n }\n\n // A path cannot carry a protocol prefix; `/\\thttps:` and friends are caught\n // above, this catches `/foo:bar` forms that some parsers read as an authority.\n return !/^\\/[^/?#]*:/.test(returnPath);\n}\n"],"mappings":";;;AAeA,SAAS,WAAW,gBAAgB;;;ACOpC,IAAM,yBAAyB;AAexB,SAAS,iBAAiB,YACjC;AACI,MAAI,CAAC,WAAW,WAAW,GAAG,GAC9B;AACI,WAAO;AAAA,EACX;AAEA,MAAI,WAAW,WAAW,IAAI,KAAK,WAAW,SAAS,IAAI,GAC3D;AACI,WAAO;AAAA,EACX;AAEA,MAAI,WAAW,SAAS,IAAI,KAAK,uBAAuB,KAAK,UAAU,GACvE;AACI,WAAO;AAAA,EACX;AAIA,SAAO,CAAC,cAAc,KAAK,UAAU;AACzC;;;ADmFmB,wBAIP,YAJO;AAjHnB,SAAS,WAAW,OACpB;AACI,SAAO,SAAS,iBAAiB,KAAK,IAAI,QAAQ;AACtD;AA+BO,SAAS,cAAc;AAAA,EAC1B,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ,GACA;AACI,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,IAAI;AAE/C,YAAU,MACV;AACI,mBAAe,gBACf;AACI,UACA;AACI,cAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,cAAM,SAAS,OAAO,IAAI,QAAQ;AAClC,cAAM,QAAQ,OAAO,IAAI,OAAO;AAChC,cAAM,YAAY,WAAW,OAAO,IAAI,WAAW,CAAC;AACpD,cAAM,aAAa,OAAO,IAAI,OAAO;AAGrC,YAAI,YACJ;AACI,gBAAM,IAAI,MAAM,UAAU;AAAA,QAC9B;AAEA,YAAI,CAAC,UAAU,CAAC,OAChB;AACI,gBAAM,IAAI,MAAM,6BAA6B;AAAA,QACjD;AAGA,cAAM,WAAW,MAAM,MAAM,GAAG,WAAW,kBAAkB;AAAA,UACzD,QAAQ;AAAA,UACR,SAAS;AAAA,YACL,gBAAgB;AAAA,UACpB;AAAA,UACA,aAAa;AAAA,UACb,MAAM,KAAK,UAAU;AAAA,YACjB,MAAM;AAAA,cACF;AAAA,cACA;AAAA,cACA;AAAA,YACJ;AAAA,UACJ,CAAC;AAAA,QACL,CAAC;AAED,YAAI,CAAC,SAAS,IACd;AACI,gBAAMA,QAAO,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACnD,gBAAM,IAAI,MAAMA,MAAK,WAAW,0BAA0B;AAAA,QAC9D;AAEA,cAAM,OAAO,MAAM,SAAS,KAAK;AAEjC,oBAAY,MAAM;AAGlB,eAAO,SAAS,OAAO,WAAW,KAAK,aAAa,SAAS;AAAA,MACjE,SACO,KACP;AACI,cAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,iBAAS,OAAO;AAChB,qBAAa,KAAK;AAClB,kBAAU,OAAO;AAAA,MACrB;AAAA,IACJ;AAEA,kBAAc;AAAA,EAClB,GAAG,CAAC,aAAa,WAAW,OAAO,CAAC;AAEpC,MAAI,OACJ;AACI,QAAI,gBACJ;AACI,aAAO,gCAAG,yBAAe,KAAK,GAAE;AAAA,IACpC;AAEA,WACI,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,WAAW,SAAS,GAC/C;AAAA,0BAAC,QAAG,kCAAoB;AAAA,MACxB,oBAAC,OAAE,OAAO,EAAE,OAAO,MAAM,GAAI,iBAAM;AAAA,MACnC,oBAAC,YAAO,SAAS,MAAM,OAAO,SAAS,OAAO,KAAK,qBAEnD;AAAA,OACJ;AAAA,EAER;AAEA,MAAI,WACJ;AACI,QAAI,kBACJ;AACI,aAAO,gCAAG,4BAAiB;AAAA,IAC/B;AAEA,WACI,oBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,WAAW,SAAS,GAC/C,8BAAC,OAAE,0CAA4B,GACnC;AAAA,EAER;AAEA,SAAO;AACX;","names":["data"]}
@@ -1,8 +1,9 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { S as SessionData } from '../session-DTHahDQ9.js';
3
+ import { S as SessionData } from '../session-Dfwu5g2W.js';
4
+ export { i as isSafeReturnPath } from '../session-Dfwu5g2W.js';
4
5
  import { K as KeyAlgorithmType } from '../types-DYyhze28.js';
5
- import { NextRequest, NextResponse } from 'next/server';
6
+ import { NextResponse, NextRequest } from 'next/server';
6
7
 
7
8
  interface RequireAuthProps {
8
9
  /**
@@ -278,6 +279,61 @@ declare function getPendingSession(): Promise<PendingSessionData | null>;
278
279
  */
279
280
  declare function clearPendingSession(): Promise<void>;
280
281
 
282
+ /**
283
+ * Session cookie names for Next.js
284
+ *
285
+ * The names carry the `SPFN_PORT` suffix, so they are only knowable at call
286
+ * time — this module is the one place an app reads them from.
287
+ */
288
+
289
+ /**
290
+ * The cookie names that make up a browser session
291
+ */
292
+ interface SessionCookieNames {
293
+ /** Encrypted session data */
294
+ session: string;
295
+ /** Current key ID (for key rotation) */
296
+ keyId: string;
297
+ /** Pending OAuth session — present only mid-flow */
298
+ oauthPending: string;
299
+ /** CSRF token — the only one the browser can read */
300
+ csrf: string;
301
+ }
302
+ /**
303
+ * Names of the cookies that make up a browser session
304
+ *
305
+ * Read at call time, never at import: the names carry the `SPFN_PORT` suffix,
306
+ * and an app that spells them itself keeps clearing the old name after a
307
+ * release renames one.
308
+ *
309
+ * @example
310
+ * ```typescript
311
+ * const names = sessionCookieNames();
312
+ * const raw = request.cookies.get(names.session);
313
+ * ```
314
+ */
315
+ declare function sessionCookieNames(): SessionCookieNames;
316
+ /**
317
+ * Expire every session cookie on a response
318
+ *
319
+ * For the route handler or middleware that answers "the API refused your
320
+ * session" — it empties the jar so the next request arrives anonymous. The
321
+ * path matches the one the setters use, because a delete under a different
322
+ * path leaves the cookie in place. Absent cookies are not an error.
323
+ *
324
+ * @param response - Response to expire the cookies on
325
+ * @returns The same response, so the call chains
326
+ *
327
+ * @example
328
+ * ```typescript
329
+ * export function GET(): NextResponse
330
+ * {
331
+ * return clearSessionCookies(NextResponse.redirect(new URL('/login', request.url)));
332
+ * }
333
+ * ```
334
+ */
335
+ declare function clearSessionCookies(response: NextResponse): NextResponse;
336
+
281
337
  /**
282
338
  * OAuth Handlers for Next.js
283
339
  *
@@ -314,4 +370,4 @@ interface OAuthCallbackOptions {
314
370
  */
315
371
  declare function createOAuthCallbackHandler(options?: OAuthCallbackOptions): (request: NextRequest) => Promise<NextResponse>;
316
372
 
317
- export { type OAuthCallbackOptions, type PendingSessionData, type PublicSession, RequireAuth, type RequireAuthProps, RequirePermission, type RequirePermissionProps, RequireRole, type RequireRoleProps, type SaveSessionOptions, SessionData, clearPendingSession, clearSession, createOAuthCallbackHandler, getAuthSessionData, getPendingSession, getSession, getUserPermissions, getUserRole, hasAnyPermission, hasAnyRole, saveSession, sealPendingSession, unsealPendingSession };
373
+ export { type OAuthCallbackOptions, type PendingSessionData, type PublicSession, RequireAuth, type RequireAuthProps, RequirePermission, type RequirePermissionProps, RequireRole, type RequireRoleProps, type SaveSessionOptions, type SessionCookieNames, SessionData, clearPendingSession, clearSession, clearSessionCookies, createOAuthCallbackHandler, getAuthSessionData, getPendingSession, getSession, getUserPermissions, getUserRole, hasAnyPermission, hasAnyRole, saveSession, sealPendingSession, sessionCookieNames, unsealPendingSession };
@@ -404,11 +404,47 @@ async function RequirePermission({
404
404
  return /* @__PURE__ */ jsx3(Fragment3, { children });
405
405
  }
406
406
 
407
+ // src/nextjs/cookie-names.ts
408
+ function sessionCookieNames() {
409
+ return {
410
+ session: COOKIE_NAMES.SESSION,
411
+ keyId: COOKIE_NAMES.SESSION_KEY_ID,
412
+ oauthPending: COOKIE_NAMES.OAUTH_PENDING,
413
+ csrf: COOKIE_NAMES.CSRF
414
+ };
415
+ }
416
+ function clearSessionCookies(response) {
417
+ for (const name of Object.values(sessionCookieNames())) {
418
+ response.cookies.delete({ name, path: "/" });
419
+ }
420
+ return response;
421
+ }
422
+
407
423
  // src/nextjs/oauth-handlers.ts
408
424
  import { NextResponse } from "next/server";
409
425
  import { cookies as cookies2 } from "next/headers.js";
410
426
  import { env as env5 } from "@spfn/core/config";
411
427
  import { logger as logger2 } from "@spfn/core/logger";
428
+
429
+ // src/lib/return-path.ts
430
+ var URL_STRIPPED_CHARACTER = /[\t\n\r]/;
431
+ function isSafeReturnPath(returnPath) {
432
+ if (!returnPath.startsWith("/")) {
433
+ return false;
434
+ }
435
+ if (returnPath.startsWith("//") || returnPath.includes("\\")) {
436
+ return false;
437
+ }
438
+ if (returnPath.includes("..") || URL_STRIPPED_CHARACTER.test(returnPath)) {
439
+ return false;
440
+ }
441
+ return !/^\/[^/?#]*:/.test(returnPath);
442
+ }
443
+
444
+ // src/nextjs/oauth-handlers.ts
445
+ function safeReturnUrl(requested, defaultRedirect) {
446
+ return requested && isSafeReturnPath(requested) ? requested : defaultRedirect;
447
+ }
412
448
  function createOAuthCallbackHandler(options) {
413
449
  const defaultRedirect = options?.defaultRedirectUrl || "/";
414
450
  const errorRedirect = options?.errorRedirectUrl || "/auth/error";
@@ -416,7 +452,7 @@ function createOAuthCallbackHandler(options) {
416
452
  const searchParams = request.nextUrl.searchParams;
417
453
  const userId = searchParams.get("userId");
418
454
  const keyId = searchParams.get("keyId");
419
- const returnUrl = searchParams.get("returnUrl") || defaultRedirect;
455
+ const returnUrl = safeReturnUrl(searchParams.get("returnUrl"), defaultRedirect);
420
456
  const error = searchParams.get("error");
421
457
  if (error) {
422
458
  const errorUrl = new URL(errorRedirect, request.url);
@@ -487,6 +523,7 @@ export {
487
523
  RequireRole,
488
524
  clearPendingSession,
489
525
  clearSession,
526
+ clearSessionCookies,
490
527
  createOAuthCallbackHandler,
491
528
  getAuthSessionData,
492
529
  getPendingSession,
@@ -495,8 +532,10 @@ export {
495
532
  getUserRole,
496
533
  hasAnyPermission,
497
534
  hasAnyRole,
535
+ isSafeReturnPath,
498
536
  saveSession,
499
537
  sealPendingSession,
538
+ sessionCookieNames,
500
539
  unsealPendingSession
501
540
  };
502
541
  //# sourceMappingURL=server.js.map