@spfn/auth 0.3.0-beta.2 → 0.3.0-beta.20

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.
Files changed (43) hide show
  1. package/README.md +1382 -23
  2. package/dist/client-proof.d.ts +45 -15
  3. package/dist/client-proof.js +198 -4
  4. package/dist/client-proof.js.map +1 -1
  5. package/dist/client.d.ts +92 -1
  6. package/dist/client.js +58 -0
  7. package/dist/client.js.map +1 -1
  8. package/dist/config.d.ts +302 -0
  9. package/dist/config.js +134 -4
  10. package/dist/config.js.map +1 -1
  11. package/dist/errors.d.ts +370 -3
  12. package/dist/errors.js +245 -2
  13. package/dist/errors.js.map +1 -1
  14. package/dist/index.d.ts +185 -2
  15. package/dist/index.js +256 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/machine-principals-BD4tnASp.d.ts +2739 -0
  18. package/dist/nextjs/api.js +350 -12
  19. package/dist/nextjs/api.js.map +1 -1
  20. package/dist/nextjs/client.d.ts +28 -1
  21. package/dist/nextjs/client.js +24 -3
  22. package/dist/nextjs/client.js.map +1 -1
  23. package/dist/nextjs/server.d.ts +173 -3
  24. package/dist/nextjs/server.js +372 -10
  25. package/dist/nextjs/server.js.map +1 -1
  26. package/dist/server.d.ts +3761 -414
  27. package/dist/server.js +5865 -1043
  28. package/dist/server.js.map +1 -1
  29. package/dist/{session-DTHahDQ9.d.ts → session-Dfwu5g2W.d.ts} +28 -1
  30. package/migrations/20260810112144_colorful_tomorrow_man/migration.sql +18 -0
  31. package/migrations/20260810112144_colorful_tomorrow_man/snapshot.json +3576 -0
  32. package/migrations/20260901091716_fine_arclight/migration.sql +21 -0
  33. package/migrations/20260901091716_fine_arclight/snapshot.json +3849 -0
  34. package/migrations/20260906155957_natural_moonstone/migration.sql +33 -0
  35. package/migrations/20260906155957_natural_moonstone/snapshot.json +4275 -0
  36. package/migrations/20260907020904_giant_eternals/migration.sql +21 -0
  37. package/migrations/20260907020904_giant_eternals/snapshot.json +4561 -0
  38. package/migrations/20260907044807_eminent_angel/migration.sql +2 -0
  39. package/migrations/20260907044807_eminent_angel/snapshot.json +4561 -0
  40. package/migrations/20260918083158_foamy_roughhouse/migration.sql +55 -0
  41. package/migrations/20260918083158_foamy_roughhouse/snapshot.json +5271 -0
  42. package/package.json +9 -6
  43. package/dist/authenticate-55LeXHqZ.d.ts +0 -1447
@@ -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,118 @@ 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
+ /**
374
+ * @spfn/auth - OAuth 2.1 consent screen (Next.js route handlers)
375
+ *
376
+ * The authorization server lives on the API origin; this is the one piece of it
377
+ * that cannot, because consent is a decision only the signed-in person can make
378
+ * and the session cookie is on the web app. `GET` draws the screen, `POST` takes
379
+ * the answer, and neither of them decides anything: both forward the request to
380
+ * `/_auth/oauth2/authorize`, which validates it against the registration and
381
+ * hands back either what to draw or the refusal to act on.
382
+ *
383
+ * Three rules shape everything below, and each of them is an attack that would
384
+ * otherwise work:
385
+ *
386
+ * - **The only URLs this file redirects to are `loginPath` and a URI the API
387
+ * returned.** The request's own `redirect_uri` is forwarded and never built
388
+ * into a `Location` — an unregistered one is exactly the open redirect the
389
+ * registration check exists to close, and the API is the only side that can
390
+ * tell the two apart.
391
+ * - **Everything interpolated into the page is escaped.** `client_name` arrives
392
+ * from unauthenticated dynamic registration, and `state` and `resource` come
393
+ * from the query string of a link somebody was sent.
394
+ * - **The POST carries its own CSRF token.** The handler's server-side call to
395
+ * the API mints the CSRF header itself and so would always pass; the check
396
+ * that matters is the browser form's, and it is made before the API is called
397
+ * at all.
398
+ */
399
+
400
+ /** One scope, with the sentence the consent screen reads aloud for it. */
401
+ interface OAuth2ConsentScope {
402
+ name: string;
403
+ description: string;
404
+ }
405
+ /**
406
+ * Everything a consent screen needs, raw and unescaped.
407
+ *
408
+ * A custom `render` receives this and owns the whole body, so it must echo
409
+ * `fields` and `csrfToken` back as hidden inputs: the POST is refused without
410
+ * the token, and the API re-validates the request from the fields rather than
411
+ * trusting what the GET was once shown.
412
+ *
413
+ * Every string here is caller-supplied. Put each one through {@link escapeHtml}.
414
+ */
415
+ interface OAuth2ConsentView {
416
+ /** Registered name of the client asking. Unauthenticated input. */
417
+ clientName: string;
418
+ /** Host the code would be sent to — the one fact about the client that is checkable. */
419
+ redirectHost: string;
420
+ scopes: OAuth2ConsentScope[];
421
+ /** RFC 8707 target the token would be good against. */
422
+ resource: string;
423
+ /** Every authorize parameter the request carried, verbatim, to echo as hidden inputs. */
424
+ fields: Record<string, string>;
425
+ /** Value the POST's `csrf` field must carry. */
426
+ csrfToken: string;
427
+ }
428
+ /**
429
+ * Options for {@link createOAuth2AuthorizeHandlers}
430
+ */
431
+ interface OAuth2AuthorizeHandlerOptions {
432
+ /**
433
+ * Where to send a visitor with no session, e.g. `/login`
434
+ *
435
+ * The handler appends `?returnUrl=` pointing at this request, so the login
436
+ * lands back on the consent screen with its parameters intact.
437
+ */
438
+ loginPath: string;
439
+ /**
440
+ * Replace the default consent page body
441
+ *
442
+ * Status, headers and the field set stay the handler's; this owns the HTML.
443
+ */
444
+ render?: (view: OAuth2ConsentView) => string;
445
+ }
446
+ /** The pair a route file re-exports as `export const { GET, POST } = ...`. */
447
+ interface OAuth2AuthorizeHandlers {
448
+ GET: (request: NextRequest) => Promise<NextResponse>;
449
+ POST: (request: NextRequest) => Promise<NextResponse>;
450
+ }
451
+ /**
452
+ * Escape a string for interpolation into HTML text or a quoted attribute.
453
+ *
454
+ * Exported because a custom `render` needs the same escaping the default body
455
+ * applies: `client_name` comes from unauthenticated dynamic registration, and
456
+ * `state` is whatever was in the link the browser followed.
457
+ *
458
+ * @param value - Raw string
459
+ * @returns The same string with `& < > " '` replaced by entities
460
+ */
461
+ declare function escapeHtml(value: string): string;
462
+ /**
463
+ * Create the consent screen's route handlers
464
+ *
465
+ * `GET` renders the screen for an `/oauth/authorize` request and `POST` takes
466
+ * the form it submits. Mount both at the path published as
467
+ * `authorization_endpoint` in the authorization server metadata —
468
+ * `/oauth/authorize` unless `authorizationServer.authorizeUrl` says otherwise.
469
+ *
470
+ * Every answer carries `Cache-Control: no-store`; every page also carries
471
+ * `Content-Security-Policy: frame-ancestors 'none'`, because a consent screen
472
+ * that can be framed is a consent screen that can be clickjacked.
473
+ *
474
+ * @param options - Where to send an unauthenticated visitor, and an optional renderer
475
+ * @returns `{ GET, POST }`, ready to re-export from a route file
476
+ *
477
+ * @example
478
+ * ```typescript
479
+ * // app/oauth/authorize/route.ts
480
+ * import { createOAuth2AuthorizeHandlers } from '@spfn/auth/nextjs/server';
481
+ *
482
+ * export const { GET, POST } = createOAuth2AuthorizeHandlers({ loginPath: '/login' });
483
+ * ```
484
+ */
485
+ declare function createOAuth2AuthorizeHandlers(options: OAuth2AuthorizeHandlerOptions): OAuth2AuthorizeHandlers;
486
+
487
+ export { type OAuth2AuthorizeHandlerOptions, type OAuth2AuthorizeHandlers, type OAuth2ConsentScope, type OAuth2ConsentView, type OAuthCallbackOptions, type PendingSessionData, type PublicSession, RequireAuth, type RequireAuthProps, RequirePermission, type RequirePermissionProps, RequireRole, type RequireRoleProps, type SaveSessionOptions, type SessionCookieNames, SessionData, clearPendingSession, clearSession, clearSessionCookies, createOAuth2AuthorizeHandlers, createOAuthCallbackHandler, escapeHtml, getAuthSessionData, getPendingSession, getSession, getUserPermissions, getUserRole, hasAnyPermission, hasAnyRole, saveSession, sealPendingSession, sessionCookieNames, unsealPendingSession };