
# JavaScript ブラウザ SDK

`@keenauth/js` は KeenAuth のブラウザ向けコアです。シングルページアプリにユーザーをサインインさせ、トークンを保持して API を呼び出します。実行時の依存関係はなく、React コンポーネントはこのパッケージの上に構築されています。このパッケージはまだ npm に公開されていません。

## リダイレクトによるサインイン

この SDK は標準的な OpenID Connect パブリッククライアントです。

- PKCE (S256) を使用し、DPoP に紐づいたトークンを発行します。
- コールバックで `state`、`nonce`、`iss` を検証します。
- ID トークンの署名とクレームを、環境の鍵に照らして検証します。

```ts
import { KeenAuth } from "@keenauth/js";

const auth = new KeenAuth({
  issuer: "https://acme.eu.keenauth.app", // your environment's issuer, exactly
  clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
  redirectUri: "https://app.acme.example/callback",
  postLogoutRedirectUri: "https://app.acme.example/",
});

// On your sign-in button:
await auth.signInRedirect({ appState: "/orders" });

// On https://app.acme.example/callback:
const { appState } = await auth.handleRedirectCallback();
```

`handleRedirectCallback` は応答を 1 回だけ受け付けます。次の場合には `KeenAuthError` で失敗します。

- `state` または `iss` が一致しない場合。
- 保留中のサインインが 10 分より古い場合。
- ID トークンが有効でない場合。

サーバーから返されたエラーは、OAuth の `error` コードを保持する `KeenAuthOAuthError` として送出されます。

## API の呼び出し

API 用のアクセストークンを取得するには、その識別子を `resource` (RFC 8707) として渡します。これは認可リクエスト、コードとの交換、およびすべてのリフレッシュで送信され、トークンの `aud` はその識別子になります。

```ts
const auth = new KeenAuth({
  issuer: "https://acme.eu.keenauth.app",
  clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
  redirectUri: "https://app.acme.example/callback",
  resource: "https://api.acme.example", // your API's identifier
  scope: "openid profile email offline_access messages:read",
});
```

- 値は、フラグメント (`#`) を含まない絶対 URI である必要があります。記述したとおりにそのまま送信されるため、登録済みの API 識別子をそのまま使用してください。無効な値の場合、コンストラクタは `config_invalid` のコードを持つ `KeenAuthError` を送出します。
- `signInRedirect({ resource })` は、そのサインイン 1 回に限り値を上書きします。
- `resource` を指定しない場合、アプリケーションに既定の API が設定されていればサーバーはそれを使用します。設定されていない場合、API のスコープは付与されません。
- このオプションは配列も受け付けます。RFC 8707 が許容するとおり、繰り返しの `resource` パラメータとして送信されます。KeenAuth のアクセストークンは正確に 1 つのオーディエンスを持つため、サーバーは 2 つ以上の値を `invalid_target` (`KeenAuthOAuthError`) で拒否します。

```ts
const res = await auth.authorizedFetch("https://api.acme.example/v1/orders");

// Or take the token yourself; it is refreshed when it is about to expire.
const token = await auth.getAccessToken();
```

`authorizedFetch` は、トークンに紐づいた新しい証明を付けて `Authorization: DPoP` を送信します。サーバーが DPoP のノンスを要求した場合、そのノンスを付けて 1 回だけ再試行します。並行する呼び出しは 1 回のリフレッシュリクエストを共有し、リフレッシュのたびにリフレッシュトークンはローテーションされます。API でこれらのトークンを検証する方法は、[API でのトークン検証](/docs/sdk/token-verification)を参照してください。

## サインアウト

```ts
await auth.signOut();
```

`signOut` はトークンを破棄し、リフレッシュトークンを取り消します。その後、ブラウザを環境のログアウトエンドポイントへ遷移させ、`postLogoutRedirectUri` に戻します。

## トークンの保管場所

- アクセス、リフレッシュ、ID の各トークンはメモリ上にのみ保持され、localStorage、sessionStorage、IndexedDB、Cookie、URL には保存されません。ページを再読み込みすると、ユーザーはリダイレクトによって再度サインインします。SDK はセッションを復元しません。
- sessionStorage には、進行中のサインインのワンタイム値のみを保持します。
- DPoP 鍵は抽出不可の WebCrypto 鍵であり、既定ではメモリ上に保持されます。

## 埋め込みサインイン

同じオブジェクトから、埋め込みコンポーネント向けのフロント API クライアントも利用できます。

- サインインの各ステップ用の `transactions`。
- アカウントのプロフィールと `user_metadata`、メール (コード認証を含む)、パスキー、セッション用の `me`。
- パスキーのセレモニー用の `webauthn`。

これらの呼び出しは、環境の Cookie と CSRF ヘッダーを使用します。サーバーは、環境自身のログインオリジンからの呼び出しのみを受け付けます。

## リダイレクトなしのサインイン

ログインオリジン以外のページでは、`clientId` を指定してトランザクションを開始します。すると SDK は、Cookie の代わりにそのトランザクションを SDK の DPoP 鍵に紐づけます。

```ts
let tx = await auth.transactions.create({ client_id: "app_01j9...", organization: "acme" });
tx = await auth.transactions.step("identifier", { identifier: "ada@acme.example" });
tx = await auth.transactions.step("password", { password });
if (tx.state === "completed") await auth.complete(tx); // { kind: "signed_in" }
```

- アプリケーションは first-party アプリケーションである必要があり、ページのオリジンはその許可済み Web オリジンのいずれかである必要があります。
- すべての呼び出しは DPoP 証明を伴い、Cookie は使用しません。サーバーが DPoP のノンスを要求した場合、SDK はそのノンスを付けて 1 回だけ再試行します。
- SDK は PKCE の verifier、`state`、`nonce` を自身で生成し、メモリに保持します。`scope` と `resource` はオプションから取得されます。`create` には `organization` と `uiLocales` を渡せます。ここでは `resource` の値は 1 つだけ使用してください。
- `complete` は応答を検証し、同じ DPoP 鍵で認可コードを交換し、トークンをメモリに保持します。ページは遷移せず、コードがアプリケーションのコードに返されることはありません。
- すべての `step` 呼び出しは、`client_id` の有無にかかわらず、ブラウザのタイムゾーンを `Idp-Zoneinfo` ヘッダーで送信します。ステップでサインインが完了し、ユーザーがタイムゾーンを選択していない場合、KeenAuth はホステッドのサインインページと同様に、それをプロフィールの `zoneinfo` に保存します。ブラウザが利用可能なタイムゾーンを報告しない場合、ヘッダーは省略され、ステップの動作は変わりません。
- サインインに、ユーザーが確認しなければならないページ (同意、法的規約、組織の選択) が必要な場合、最後のステップは `authorization_failed` のコードを持つ `KeenAuthProblemError` で失敗します。そのユーザーには `signInRedirect` を使用してください。

## 自身のオリジンからのアカウント呼び出し

ユーザーがサインインし、SDK がリフレッシュトークンを保持している場合 (既定のスコープは `offline_access` を要求します)、`me` と `organizations` の呼び出しはサインインホストのセッション Cookie を必要としなくなります。

- SDK は `resource=<issuer>/fapi` を指定してリフレッシュし、フロント API 用の短命なアクセストークンを取得します。このトークンは DPoP に紐づき、他のトークンと同様にメモリ上にのみ保持されます。
- 各アカウント呼び出しは、`Authorization: DPoP <token>` と、その呼び出し用の DPoP 証明を Cookie なしで送信します。サーバーが DPoP のノンスを要求した場合、SDK はそのノンスを付けて 1 回だけ再試行します。トークンが無効になっている場合は、新しいトークンを取得して 1 回だけ再試行します。
- リフレッシュトークンはリフレッシュのたびにローテーションされるため、SDK はリフレッシュを 1 つずつ順番に実行します。
- これは、許可済み Web オリジンにページのオリジンを含む first-party アプリケーションでのみ機能します。
- トークンがない場合、SDK は従来どおりセッション Cookie を使用します。

サーバー側の規則については、[フロント API アクセストークン](/docs/api/fapi/front-api-token)を参照してください。

## 組織の切り替え

`organizations.switch(orgId)` は、ページを遷移させずにサインイン中のユーザーを別の組織へ移します。`organizations.listMine()` から取得した組織の `org_…` ID を渡してください。

```js
const { data } = await auth.organizations.listMine();
await auth.organizations.switch(data[1].id);
```

- 上記のアカウント呼び出しと同じ条件が必要です。リフレッシュトークン、`redirectUri`、そしてページのオリジンを許可済み Web オリジンに含む first-party アプリケーションです。
- SDK はフロント API トークンを用いて [切り替えリクエスト](/docs/api/fapi/transactions/create)を送信し、同じ鍵の証明を付けてコードを交換し、`org_id` が対象組織となる新しいトークンを保持します。これらすべてが成功するまで、保持していたトークンはそのまま残ります。
- 組織がユーザーの確認を必要とするページ (MFA、SSO、同意) を必要とする場合、呼び出しは `authorization_failed` のコードを持つ `KeenAuthProblemError` で失敗します。そのユーザーには `signInRedirect({ organization })` を使用してください。

## 現在の制限

- プロフィールの書き込み (`me.update`、`me.updateMetadata`) は、直近のプロフィールの読み取りまたは書き込みで得た `ETag` を `If-Match` に付けて送信します。その間にプロフィールが別の場所で変更されていた場合、書き込みは `precondition_failed` のコードを持つ `KeenAuthProblemError` で失敗します。再試行する前に `me.get()` を再度呼び出してください。
- 電話番号はまだ利用できません。`me.phones` は `undefined` です。
