初めてのOSSコントリビュート【vue3, sidebase/nuxt-auth】

## はじめに
お疲れ様です。お久しぶりです。FAL（[X: @fal_engineer](https://x.com/fal_engineer)）です。
半年以上前になるんですが、初のOSSコントリビュートを出来たため今更ながら記事に残そうと思います。
**ちなみに自分は英語できません。正直中学レベル未満だと思います。**
いまだにBe動詞の必要性がいまいちわかりません。
たまに外人さんに道を尋ねられたりしますが、完全に日本語で返事しながらパッションによる会話をしています。何とかなります。

## この記事の対象読者
- OSSコントリビュートしてみたいが、具体的なネクストアクションが見えず困っている方

## きっかけ
当時、本業で自社サービスのフロントエンドのvue2 → vue3（正確にはNuxtBridge → Nuxt3）移行PJを担当させていただいていました。
（⇧実作業内容・選定理由について詳細に記述した記事が[こちら](https://xmart-techblog.hatenablog.com/entry/2024/03/18/110018)）

⇧の記事にも記述している通り、サービスはSPAでログイン画面以外認証必須なWebアプリです。
認証の機構、アカウント情報は全て同自社サービスのAPIサーバー・DBが保持しており、認証はJWT認証です。

また、マルチプロダクト化を推進している背景上、複数のフロント環境を立ち上げることがあります。

選定した`@sidebase/nuxt-auth`で、Cookieにトークンを保存する際のkey名のカスタマイズが出来ず、
ローカル環境で複数のNuxt3を立ち上げた際にトークンが上書きされてしうため、「この機にPR出してみるか〜」となりました。

## やったこと
### コントリビュートガイドを探す
大きなOSSだと大抵あるっぽいっす。
コントリビュートする際の作業手順のまとめですね。

当該リポジトリではREADMEに書いてありました。

https://github.com/sidebase/nuxt-auth?tab=readme-ov-file#contributing

`.github/CONTRIBUTING.md`を読んでみると、ブランチの命名規約や、コミット時のルール、PRのPrefixについて書かれていました。

### リポジトリをフォーク
https://docs.github.com/ja/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo

リポジトリのフォークの必要性について、わかりやすくまとめられていたためリンクを添付させていただきます。

https://qiita.com/iverson3kobe0824/items/3e95cc5476b1c621fe95#%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%81%AE%E3%83%95%E3%82%A9%E3%83%BC%E3%82%AF

フォーク後のリポジトリがこんな感じになります。
なってました。

https://github.com/FAL-coffee/nuxt-auth

### 作業・テスト

ローカルにリポジトリをcloneし、いつものようにコードをガリガリ書いたり、テスト走らせたりしていきます。

`npm link`とかは初めて知ったコマンドでした。とっても便利。というか、パッケージの開発においては、これが無くては開発出来ないです。

`npm link`についてわかりやすくまとめられていたため、添付させていただきます。

https://qiita.com/k_kind/items/b3d04bd5a47ee26b6e2d

当時のコードをベースにどの辺読んだかもさらっと程度に書きます。

#### やりたいこと

`@sidebase/nuxt-auth`を使用する場合は`nuxt.config.ts`から`auth. ~`に設定を書いていくことになります。
保存するJWTトークン、リフレッシュトークンの保存名を変えたいため、
auth.token.cookieNameとかauth.token.propertyNameとかを設定できるようにしたいですね。

#### 各playground dirのconfig
モジュールに`src/module.ts`を指定し、`provider.type = 'refresh'`を使用していることがわかります。
同様に、`playground-local/nuxt.config.ts`では`provider.type = 'local'`となっています。
```ts:playground-refresh/nuxt.config.ts
export default defineNuxtConfig({
  modules: ['../src/module.ts'],
  auth: {
    provider: {
      type: 'refresh',
```

#### 実際のmoduleファイル

```ts:src/module.ts
export default defineNuxtModule<ModuleOptions>({
  meta: {
    name: PACKAGE_NAME,
    configKey: 'auth'
  },
  setup (userOptions, nuxt) {
    const logger = useLogger(PACKAGE_NAME)

    // 0. Assemble all options
    const { origin, pathname = '/api/auth' } = getOriginAndPathnameFromURL(
      userOptions.baseURL ?? ''
    )

    const selectedProvider = userOptions.provider?.type ?? 'authjs'

    const options = {
      ...defu(userOptions, topLevelDefaults, {
        computed: {
          origin,
          pathname,
          fullBaseUrl: joinURL(origin ?? '', pathname)
        }
      }),
      // We use `as` to infer backend types correctly for runtime-usage (everything is set, although for user everything was optional)
      provider: defu(
        userOptions.provider,
        // ⭐️ ココ ⭐️ 
        defaultsByBackend[selectedProvider]
      ) as DeepRequired<AuthProviders>
    }
```

JWT認証を使用する場合はlocal, refreshのいずれかの設定を使用するため、この辺りの実装を見れば良さそうです。

```ts:src/module.ts
  local: {
    type: 'local',
    pages: {
      login: '/login'
    },
    endpoints: {
      signIn: { path: '/login', method: 'post' },
      signOut: { path: '/logout', method: 'post' },
      signUp: { path: '/register', method: 'post' },
      getSession: { path: '/session', method: 'get' }
    },
    token: {
      signInResponseTokenPointer: '/token',
      type: 'Bearer',
      headerName: 'Authorization',
      maxAgeInSeconds: 30 * 60,
      sameSiteAttribute: 'lax'
    },
    sessionDataType: { id: 'string | number' }
  },

  refresh: {
    type: 'refresh',
    pages: {
      login: '/login'
    },
    refreshOnlyToken: true,
    endpoints: {
      signIn: { path: '/login', method: 'post' },
      signOut: { path: '/logout', method: 'post' },
      signUp: { path: '/register', method: 'post' },
      getSession: { path: '/session', method: 'get' },
      refresh: { path: '/refresh', method: 'post' }
    },
    token: {
      signInResponseTokenPointer: '/token',
      type: 'Bearer',
      headerName: 'Authorization',
      maxAgeInSeconds: 5 * 60,
      sameSiteAttribute: 'none' // 5 minutes
    },
    refreshToken: {
      signInResponseRefreshTokenPointer: '/refreshToken',
      maxAgeInSeconds: 60 * 60 * 24 * 7 // 7 days
    },
    sessionDataType: { id: 'string | number' }
  },
```

#### トークンのCookieへのセット / 取得部分
トークンはデフォルトでは`auth:token, auth:refresh-token`的な感じで保存されていました。
検索してみたら簡単に出てきましたね。この辺を変えれば良さそうです

```ts:src/runtime/composables/refresh/useAuthState.ts
interface UseAuthStateReturn extends CommonUseAuthStateReturn<SessionData> {
  token: ComputedRef<string | null>
  rawToken: CookieRef<string | null>,
  setToken: (newToken: string | null) => void
  clearToken: () => void
}

export const useAuthState = (): UseAuthStateReturn => {
  const config = useTypedBackendConfig(useRuntimeConfig(), 'local')
  const commonAuthState = makeCommonAuthState<SessionData>()

  // Re-construct state from cookie, also setup a cross-component sync via a useState hack, see https://github.com/nuxt/nuxt/issues/13020#issuecomment-1397282717
  const _rawTokenCookie = useCookie<string | null>('auth:token', { default: () => null, maxAge: config.token.maxAgeInSeconds, sameSite: config.token.sameSiteAttribute })
```

#### ドキュメントへの追記 / 変更

機能を追加する以上、ドキュメントファイルをいじる必要がありそうです。

`nuxt-auth/docs/content/2.configuration/2.nuxt-config.md`こんな感じのファイルがありました。内容はちょっと長いので添付しません。

## 実際のPRがこれ

https://github.com/sidebase/nuxt-auth/pull/654

### AIめっちゃ便利

序盤の伏線回収になります。

> **ちなみに自分は英語できません。正直中学レベル未満だと思います。**
いまだにBe動詞の必要性がいまいちわかりません。
たまに外人さんに道を尋ねられたりしますが、完全に日本語で返事しながらパッションによる会話をしています。何とかなります。

メンテナーさんとのコミュニケーションやPRに書いてるあれこれは全部ChatGPTに書いてもらいました。
DeepLに一旦通して確認したりはしましたが、その程度です。
AIまじでめっちゃ便利。