@auth-link/web 0.0.4 → 0.0.5

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.
package/docs/README.ja.md CHANGED
@@ -2,404 +2,281 @@
2
2
 
3
3
  [English](../README.md) | [简体中文](./README.zh-CN.md) | **日本語**
4
4
 
5
- **AuthLink のスマートフォン登録とアプリでの確認**を Web サイトに組み込む React / Vue コンポーネントです。登録用 QR コード、同期する 6 桁の照合コード、カウントダウン、確認結果を表示します。特定のサイト基盤には依存しません。
5
+ スマートフォン登録とアプリ内確認のフロントエンド用ダイアログです。Vue 3、React 18/19、通常の JavaScript に対応します。インストール、session 取得、client 初期化、表示、イベントと後処理の順に接続します。
6
6
 
7
- ## Go + Vue の接続手順
8
-
9
- Go + Vue の実際の接続手順は [ログイン、端末登録、確認後の公開](./go-vue.ja.md)、完全なコンポーネントは [Vue CMS 例](../examples/vue-cms.vue) を参照してください。
10
-
11
- サイトのログイン → Go `/api/session` の authenticated/csrf → `csrfToken: () => session.csrf` で別 origin のクライアント設定 → bind/confirm 作成 → Go で消費・検証・DB コミット → ダイアログを閉じる → 独立した業務 API で保存済み確認結果を再検証して公開、という順序です。baseUrl は Go、authLinkOrigin は AuthLink を指し、テナントキーは Go に保持します。記事/版付き要求は独自 fetch と validateTicket を使い、SDK create は purpose のみ送ります。
12
-
13
- ## 主な機能
14
-
15
- - ブラウザー内で生成するスマートフォン登録用 QR コード。
16
- - アプリと同期し、30 秒ごとに更新される照合コード。
17
- - 承認・拒否・期限切れ・エラーのイベント。
18
- - React コンポーネントと hook、Vue コンポーネントと composable、フレームワーク非依存の API。
19
- - **簡体字中国語を既定とする 13 言語**。コンポーネントごとに指定可能。
20
- - TypeScript、独立した CSS、任意の React / Vue peer dependency。
21
-
22
- 本パッケージはブラウザー用の接続ライブラリです。サイトのバックエンド、AuthLink サービス、モバイルアプリと併用します。AuthLink はアプリ内の承認と元の端末のハードウェア証明を検証します。システムのパスキー、指紋認証、画面ロックは要求しません。
23
-
24
- ## インストール
7
+ ## 1. インストールと 2 つの API
25
8
 
26
9
  ```sh
27
- npm install @auth-link/web
10
+ npm install @auth-link/web@latest
11
+ # 利用する vue、または react / react-dom もインストールします。
28
12
  ```
29
13
 
30
- 対応環境:React 18.2+ / 19、Vue 3.3+、TypeScript、ESM。利用するフレームワークだけをインストールしてください。ツール環境には Node 22.13+ が必要です。
31
-
32
- ## 接続の流れ
33
-
34
- 1. サイトのバックエンドに AuthLink のテナント資格情報を設定します。
35
- 2. 下記の業務バックエンド(同一オリジンまたは明示的に許可した別オリジン)の作成・結果取得 API を実装します。
36
- 3. ユーザー操作でリクエストを作成し、取得した ticket をコンポーネントに渡します。
37
- 4. バックエンドが結果を検証・消費した後に確認完了を返します。
38
-
39
- ブラウザーからこのライブラリを呼び出せますが、テナント API キーと結果の消費処理は必ずサーバー側に置きます。標準クライアントは既定の同一オリジンと、明示的に許可した別オリジンに対応します。独自の通信方式では `AuthLinkClient` を実装できますが、資格情報と検証をブラウザーに移さないでください。
40
-
41
- ## クライアント作成(既定の同一オリジン Cookie モード)
42
-
43
- `readCsrfTokenFromYourCms` は SDK のエクスポートではなく、接続先サイトで実装する関数です。現在のログインセッションに対してバックエンドが発行した CSRF token を読み取ります。CMS は必須ではありません。ブラウザーで値を生成したり、AuthLink の API キーを返したりしないでください。
44
-
45
- 既存フレームワークの CSRF 機構を優先してください。独自に実装する場合は、サーバーで暗号学的乱数(Node の `randomBytes(32).toString('base64url')` など)を生成し、セッションに保存して HTML 属性をエスケープしたうえでページに埋め込みます。下記の `{{ csrfToken }}` はサーバーテンプレートの変数です。このページを公開キャッシュしないでください。
14
+ | origin | フロントエンド設定 | 用途 |
15
+ | --- | --- | --- |
16
+ | 自サイトの API(例 https://cms-api.example.com) | baseUrl、allowedBackendOrigins | session、要求作成、結果 |
17
+ | AuthLink 本番 API https://api.getauthlink.com | authLinkOrigin | ticket 内のサービス origin の照合 |
46
18
 
47
- ```html
48
- <meta name="csrf-token" content="{{ csrfToken }}">
49
- ```
19
+ baseUrl は自サイトの業務 API であり、AuthLink API や公式サイトではありません。既存のログイン/session と下記要求 API を利用します。SDK 自体はログイン/session API を提供しません。テナントキーはブラウザーに渡しません。
50
20
 
51
- SDK は各リクエストで token を読み取り、`X-CSRF-Token` と同一オリジンのセッション cookie を送信します。作成 API と結果 API の両方で、バックエンドが同じセッションの token と照合し、欠落・不一致や不正なリクエスト元を拒否する必要があります。ログインやセッション変更後は最新 token を取得してください。SPA ではサイト独自の同一オリジン初期化 API から取得する方法も使えます。別のヘッダー名を使うフレームワークでは`csrfHeaderName` または独自 `AuthLinkClient` で適合します。
21
+ ルート import は client と型、/vue と /react はコンポーネントと useAuthLink、/styles.css は明示 import です。ESM、TypeScript、ツール環境 Node 22.13+、React 18.2+/19 または Vue 3.3+。利用するフレームワークのみ導入します。
52
22
 
53
- `authlink-client.ts` を作成します:
23
+ ## 2. 現在の session と client
54
24
 
55
25
  ```ts
56
- // authlink-client.ts
57
26
  import { createAuthLinkClient } from '@auth-link/web';
58
27
  import '@auth-link/web/styles.css';
59
28
 
60
- // The application server renders this meta element using the current session's
61
- // CSRF token. This function only reads that token; it does not generate one.
62
- export function readCsrfTokenFromYourCms(): string {
63
- const token = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content;
64
- if (!token) throw new Error('Missing CSRF token; load the authenticated application page first.');
65
- return token;
66
- }
67
-
68
- export const client = createAuthLinkClient({
69
- baseUrl: '/api/authlink',
70
- csrfToken: readCsrfTokenFromYourCms,
71
- // AuthLink production API origin.
72
- authLinkOrigin: 'https://api.getauthlink.com',
73
- });
74
- ```
75
-
76
- AuthLink の本番 API は `https://api.getauthlink.com` です。baseUrl は自サイトの業務バックエンドを指し、AuthLink 公式サイトのドメインは API に使いません。独自に配置したサービスへ接続する場合は、その設定済み origin を使用します。リクエストは下記の React 例のようにユーザー操作で作成します。
77
-
78
- ## フロントエンドとバックエンドの別オリジン対応(0.0.2 以降)
79
-
80
- 既定は同一オリジンの `/api/authlink` です。別オリジンの業務 API に直接接続するには完全な `baseUrl` と `allowedBackendOrigins` を指定します。例:フロントエンド `https://www.example.com`、バックエンド `https://api.example.com`。サブドメインやポートが異なる場合も別オリジンです。許可リストにはパス、ワイルドカード、ユーザー名・パスワードを含めず、信頼する固定設定を使います。HTTPS が必須で、開発用 HTTP loopback(localhost / 127.0.0.1 / [::1])のみ例外です。
81
-
82
- SDK は認証情報を読む前に接続先を検証し、リダイレクトを拒否します。`authLinkOrigin` は QR 内の AuthLink サービスオリジンの検証用で、業務 API の許可リストとは別です。
83
-
84
- ### 別オリジンの Cookie / session
85
-
86
- 汎用例の JSON 名は csrfToken ですが、Go CMS の実際の応答は `{ authenticated: true, csrf: '…' }` です。session.csrf を SDK の csrfToken コールバックに渡せばよく、バックエンドのフィールド名を変更する必要はありません。
87
-
88
- 下記の `GET /api/session` はサイトで実装する例示 API で、SDK が提供する API ではありません。ログイン session の `{ "csrfToken": "現在のtoken" }` を返し、`Cache-Control: no-store` と CORS を設定します。既存サイトの API パスに適合してください。
89
-
90
- ```ts
91
- import { createAuthLinkClient } from '@auth-link/web';
92
-
93
- /** /api/session is an example endpoint implemented by your application backend. */
94
- export async function createSessionBackendClient(backendOrigin: string) {
95
- const response = await fetch(`${backendOrigin}/api/session`, {
96
- credentials: 'include', cache: 'no-store', redirect: 'error',
29
+ // getCsrf reads your website's CURRENT session token on every request.
30
+ export function createWebsiteClient(getCsrf: () => string, backendOrigin?: string) {
31
+ return createAuthLinkClient({
32
+ baseUrl: backendOrigin ? `${backendOrigin}/api/authlink` : '/api/authlink',
33
+ allowedBackendOrigins: backendOrigin ? [backendOrigin] : [],
34
+ credentials: backendOrigin ? 'include' : 'same-origin',
35
+ csrfToken: getCsrf,
36
+ authLinkOrigin: 'https://api.getauthlink.com',
97
37
  });
98
- if (!response.ok) throw new Error('Session initialization failed');
99
- const session: unknown = await response.json();
100
- if (!session || typeof session !== 'object' || !('csrfToken' in session) ||
101
- typeof session.csrfToken !== 'string' || !session.csrfToken) {
102
- throw new Error('The session endpoint must return { csrfToken: string }');
103
- }
104
- let currentCsrfToken = session.csrfToken;
105
- return {
106
- client: createAuthLinkClient({
107
- baseUrl: `${backendOrigin}/api/authlink`,
108
- allowedBackendOrigins: [backendOrigin],
109
- credentials: 'include',
110
- csrfToken: () => currentCsrfToken,
111
- }),
112
- // Call after your application's session/token refresh; recreate on account changes.
113
- updateCsrfToken(value: string) { currentCsrfToken = value; },
114
- };
115
38
  }
116
39
  ```
117
40
 
118
- ログイン後に `await createSessionBackendClient('https://api.example.com')` を呼び、返された `client` を既存コンポーネントに渡します。CSRF token 更新時は `updateCsrfToken(newToken)` を呼びます。ログアウトやアカウント切替時は古いダイアログを閉じ、状態を再初期化します。別オリジン Cookie には `credentials: 'include'` が必要ですが、SameSite、Secure、domain/path、ブラウザーの第三者 Cookie 制限も適用されます。別オリジンと cross-site は同じではありません。cross-site Cookie には通常 `SameSite=None; Secure` が必要で、ブラウザーによっては引き続き拒否されます。
119
-
120
- ### Bearer token / JWT
121
-
122
- サイトの既存ログイン状態を渡してください。ログイン・更新処理で `auth.accessToken` を更新すると、SDK は作成・ポーリングのたびに最新値を読み取ります。
41
+ サイトのログイン後に既存 session API を読みます。例は CMS の authenticated/csrf です。フィールド名が csrfToken ならその値を SDK の csrfToken コールバックへ渡します。
123
42
 
124
43
  ```ts
125
- import { createAuthLinkClient } from '@auth-link/web';
126
-
127
- /** auth is your website's existing login state, updated by its login/refresh flow. */
128
- export function createBearerBackendClient(backendOrigin: string, auth: { accessToken: string }) {
129
- return createAuthLinkClient({
130
- baseUrl: `${backendOrigin}/api/authlink`,
131
- allowedBackendOrigins: [backendOrigin],
132
- credentials: 'omit',
133
- accessToken: () => auth.accessToken,
134
- });
44
+ // Run in your website's existing authenticated browser flow.
45
+ const backendOrigin = 'https://cms-api.example.com'; // YOUR application backend
46
+ const response = await fetch(`${backendOrigin}/api/session`, {
47
+ credentials: 'include', cache: 'no-store', redirect: 'error',
48
+ });
49
+ if (!response.ok) throw new Error('Session unavailable');
50
+ let session = await response.json();
51
+ if (session.authenticated !== true || typeof session.csrf !== 'string' || !session.csrf) {
52
+ throw new Error('Log in to your website first');
135
53
  }
54
+ const client = createWebsiteClient(() => session.csrf, backendOrigin);
55
+ // Same origin: createWebsiteClient(() => session.csrf)
56
+ // After token refresh, replace session or update session.csrf.
57
+ // On logout/account/session change, close/unmount old dialogs and recreate client state.
136
58
  ```
137
59
 
138
- これはサイト利用者のログイン token であり、AuthLink のテナント API キーではありません。SDK が `Authorization: Bearer ...` を付加します。`accessToken` 指定時は既定で `credentials: 'omit'` となり、Cookie にフォールバックせず Bearer のみで認証するバックエンドでは CSRF を省略できます。`include` / `same-origin` を明示して Cookie を併用する場合は CSRF の提供・検証も必要です。
139
-
140
- ### バックエンドの CORS
60
+ GET /api/session はサイト側の既存ルートの例です。同 origin は相対 URL と same-origin Cookie、別 origin の Cookie は include を使い、業務 API が現在のフロントエンド origin を許可している必要があります。Cookie 制限も適用されます。HttpOnly Cookie を読んだり、CSRF をブラウザーで生成したり、照合数字で代用したりしません。
141
61
 
142
- `OPTIONS` プリフライトを処理し、実リクエスト・エラー応答・session 初期化 API にも必要な CORS ヘッダーを返します。フロントエンドが `https://www.example.com` の例:
62
+ Bearer/JWT を使う場合、auth は既存ログイン状態です。更新された accessToken を毎回読みます。これはテナントキーではありません。credentials=omit の Bearer のみ CSRF を省略でき、Cookie 併用なら csrfToken が必要です。
143
63
 
144
- ```http
145
- Access-Control-Allow-Origin: https://www.example.com
146
- Access-Control-Allow-Credentials: true
147
- Access-Control-Allow-Methods: GET, POST, OPTIONS
148
- Access-Control-Allow-Headers: Content-Type, X-CSRF-Token, Idempotency-Key, Authorization
149
- Vary: Origin
64
+ ```ts
65
+ const backendOrigin = 'https://cms-api.example.com'; // YOUR application backend
66
+ const client = createAuthLinkClient({
67
+ baseUrl: `${backendOrigin}/api/authlink`, allowedBackendOrigins: [backendOrigin],
68
+ credentials: 'omit', accessToken: () => auth.accessToken,
69
+ authLinkOrigin: 'https://api.getauthlink.com',
70
+ });
150
71
  ```
151
72
 
152
- Cookie を使う場合 `Access-Control-Allow-Origin: *` は使用できません。バックエンドの許可リストと一致する Origin のみ返し、任意の値を反射しないでください。プリフライトには通常ログイン認証情報がないため、まず Origin・メソッド・ヘッダーを検証し、実リクエストで本人確認・CSRF(Cookie モード)・所有者を検証します。独自ヘッダーや CSRF ヘッダー名は `Access-Control-Allow-Headers` に追加します。SDK はサーバーの CORS 設定を代行できず、`no-cors` で結果を読めません。[MDN CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) を参照してください。
153
-
154
- ### クライアント設定
73
+ ## 3. Vue:クリックで作成、表示、後処理
155
74
 
156
- | 設定 | 説明 |
157
- | --- | --- |
158
- | `baseUrl` | 業務 API の接頭辞、既定 `/api/authlink`;別オリジン時は完全 URL |
159
- | `allowedBackendOrigins` | 追加許可する正確な業務 API オリジンの配列 |
160
- | `credentials` | 既定 `same-origin`、`accessToken` 指定時 `omit`;別オリジン Cookie は `include` |
161
- | `csrfToken` | リクエストごとの同期/非同期取得関数;Cookie モードは必須 |
162
- | `csrfHeaderName` | 既定 `X-CSRF-Token`、フレームワークに合わせて変更可能 |
163
- | `accessToken` | サイトのログイン token 取得関数;`Bearer ` 接頭辞は付けない |
164
- | `headers` | 追加 `HeadersInit`、またはそれを返す同期/非同期関数 |
165
- | `authLinkOrigin` | QR 内の信頼するサービスオリジン。業務 API オリジンとは別 |
166
- | `fetch` | 任意の fetch 実装;接続先検証は維持 |
75
+ `examples/vue-client.vue` を AuthLinkPanel.vue としてコピーできます。ログイン後に現在の session.csrf を渡し、別 origin なら backendOrigin も渡します。ログアウト・アカウント/session 変更では旧コンポーネントをアンマウントして再作成します。token 更新時は csrf prop を更新します。
167
76
 
168
- 追加ヘッダーは SDK 管理の `Accept`、`Content-Type`、`Authorization`、`Idempotency-Key`、設定済み CSRF ヘッダーを上書きできません(`RESERVED_HEADER`)。`accessToken` と `csrfHeaderName` を使用してください。設定型 `ClientOptions` はルートエントリーからインポートできます。
169
-
170
- 未許可オリジンは `BACKEND_ORIGIN_NOT_ALLOWED`(旧 `CMS_ORIGIN_REQUIRED` に代わる)、不正な許可設定/URL は `INVALID_BACKEND_ORIGIN` / `INVALID_BASE_URL`、token 不足は `ACCESS_TOKEN_REQUIRED` / `CSRF_REQUIRED` となります。非同期の認証情報取得にも取消しと 10 秒タイムアウトが適用されます。CORS の詳細な失敗理由は開発者ツールで確認してください。SDK では通常 `NETWORK_ERROR` になります。
171
-
172
- ## 関数でダイアログを開く(React / Vue / JavaScript)
173
-
174
- ```ts
175
- import { openAuthLinkBind, openAuthLinkConfirm, requestKey } from '@auth-link/web';
176
- import { client } from './authlink-client';
77
+ ```vue
78
+ <script setup lang="ts">
79
+ import { markRaw, onUnmounted, ref, shallowRef } from 'vue';
80
+ import { createAuthLinkClient, requestKey } from '@auth-link/web';
81
+ import type { Confirmation, Mode, Ticket } from '@auth-link/web';
82
+ import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/vue';
177
83
  import '@auth-link/web/styles.css';
178
84
 
179
- const ticket = await client.create('confirm', { idempotencyKey: requestKey() });
180
- const modal = openAuthLinkConfirm({
181
- ticket, client, locale: 'ja',
182
- onConfirmed: result => console.log(result.requestId),
183
- onClose: () => console.log('Closed'),
184
- });
185
- // 登録: openAuthLinkBind({ ticket: bindingTicket, client })
186
- // modal.close(); modal.setLocale('en');
187
- ```
188
-
189
- React / Vue コンポーネントもマウント時にダイアログを開きます。React の onClose、Vue の @close で ticket を消去してアンマウントしてください。SSR はダイアログも通信も生成しません。スマートフォンの画面幅、スクロール、閉じるボタン、Escape に対応し、ネイティブ dialog.showModal() が必要です。body 直下に表示するため、テーマ変数は :root または .authlink-dialog に設定します。
85
+ // csrf comes from your website's current authenticated session.
86
+ const props = defineProps<{ csrf: string; backendOrigin?: string }>();
87
+ const ticket = shallowRef<Ticket | null>(null);
88
+ const busy = ref(false), message = ref('');
89
+ const lifetime = new AbortController();
90
+ let creationKey: string | null = null;
91
+ let creationMode: Mode | null = null;
92
+ const client = markRaw(createAuthLinkClient({
93
+ baseUrl: props.backendOrigin ? `${props.backendOrigin}/api/authlink` : '/api/authlink',
94
+ allowedBackendOrigins: props.backendOrigin ? [props.backendOrigin] : [],
95
+ credentials: props.backendOrigin ? 'include' : 'same-origin',
96
+ csrfToken: () => props.csrf,
97
+ authLinkOrigin: 'https://api.getauthlink.com',
98
+ }));
99
+ function finish(text: string) { ticket.value = null; message.value = text; }
100
+ function confirmed(result: Confirmation) {
101
+ finish(`${result.purpose}: ${result.requestId}`);
102
+ // Reload your website's account/status API if the page needs fresh state.
103
+ }
104
+ function showError(error: Error) { message.value = error.message; }
105
+ async function start(mode: Mode) {
106
+ if (busy.value || ticket.value) return;
107
+ busy.value = true; message.value = '';
108
+ try {
109
+ if (creationMode !== mode) { creationKey = null; creationMode = mode; }
110
+ creationKey ??= requestKey();
111
+ const value = await client.create(mode, { idempotencyKey: creationKey, signal: lifetime.signal });
112
+ if (lifetime.signal.aborted) return;
113
+ ticket.value = value; creationKey = null;
114
+ } catch (error) {
115
+ if (!lifetime.signal.aborted) showError(error as Error);
116
+ // Keep the same key when retrying an uncertain creation of the same mode.
117
+ } finally { busy.value = false; }
118
+ }
119
+ onUnmounted(() => { lifetime.abort(); ticket.value = null; creationKey = null; });
120
+ </script>
190
121
 
191
- 閉じてもサーバーのリクエストを拒否・取消しません。元の有効期限は維持されます。承認・拒否時は自動で閉じ、onConfirmed または onRejected を通知します。ユーザー操作の onClose は通知しません。関数で開いた場合はページ破棄時に modal.destroy() を呼んで通信と UI を解除してください。destroy() は onClose を通知しません。modal.setLocale() はリクエストを作り直さずに言語を切り替えます。
122
+ <template>
123
+ <button :disabled="busy || !!ticket" @click="start('bind')">Bind phone</button>
124
+ <button :disabled="busy || !!ticket" @click="start('confirm')">Confirm</button>
125
+ <AuthLinkBind v-if="ticket?.purpose === 'bind'" :ticket="ticket" :client="client" variant="B"
126
+ @confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected')"
127
+ @expired="finish('Expired; start again')" @error="showError" />
128
+ <AuthLinkConfirm v-else-if="ticket?.purpose === 'confirm'" :ticket="ticket" :client="client" variant="B"
129
+ @confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected')"
130
+ @expired="finish('Expired; start again')" @error="showError" />
131
+ <p role="status">{{ message }}</p>
132
+ </template>
133
+ ```
192
134
 
135
+ ticket は shallowRef、client は markRaw で保持します。同じ用途の作成失敗ではキーを保持し、成功後に消去します。mounted/watch で要求を作りません。確認・拒否・期限切れ・閉じる操作で ticket を消去します。通信エラーでは表示を維持して同じ要求を再試行します。必要なアカウント状態は自サイトの API から再取得します。
193
136
 
194
- ## React
137
+ ## 4. React
195
138
 
196
- `showBound(requestId)` はサイトの表示更新用関数です。メッセージを更新し、ticket を消去してダイアログをアンマウントします。下記には拒否・期限切れ・閉じる処理も含まれます。`<AuthLinkPanel mode="bind" />` または `<AuthLinkPanel mode="confirm" />` で使います。
139
+ 上記で初期化した安定した client と mode=bind/confirm を渡します。ログアウトやアカウント/session 変更ではアンマウントします。
197
140
 
198
141
  ```tsx
199
142
  "use client";
200
- import { useRef, useState } from 'react';
143
+ import { useEffect, useRef, useState } from 'react';
201
144
  import { requestKey } from '@auth-link/web';
202
- import type { Mode, Ticket } from '@auth-link/web';
145
+ import type { AuthLinkClient, Mode, Ticket } from '@auth-link/web';
203
146
  import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/react';
204
- import { client } from './authlink-client';
147
+ // Import '@auth-link/web/styles.css' once in your application entry.
205
148
 
206
- export function AuthLinkPanel({ mode }: { mode: Mode }) {
149
+ export function AuthLinkPanel({ client, mode }: { client: AuthLinkClient; mode: Mode }) {
207
150
  const [ticket, setTicket] = useState<Ticket | null>(null);
208
151
  const [busy, setBusy] = useState(false);
209
152
  const [message, setMessage] = useState('');
210
- const key = useRef<string | null>(null);
211
- const creating = useRef(false);
212
- function finish(message: string) {
213
- setTicket(null);
214
- setMessage(message);
215
- }
216
- function showBound(requestId: string) {
217
- // UI feedback only. The backend has already verified and saved the binding.
218
- finish(`Phone bound (request ${requestId})`);
219
- }
220
- function showConfirmed(requestId: string) {
221
- finish(`Confirmed (request ${requestId})`);
222
- }
223
- const start = async () => {
224
- if (creating.current || ticket) return;
225
- creating.current = true;
226
- setBusy(true);
227
- setMessage('');
153
+ const creation = useRef<{ mode: Mode; key: string } | null>(null);
154
+ const inFlight = useRef(false);
155
+ const controller = useRef<AbortController | null>(null);
156
+ useEffect(() => () => { controller.current?.abort(); }, []);
157
+ function finish(text: string) { setTicket(null); setMessage(text); }
158
+ async function start() {
159
+ if (inFlight.current || ticket) return;
160
+ inFlight.current = true; setBusy(true); setMessage('');
161
+ const attempt = new AbortController(); controller.current = attempt;
228
162
  try {
229
- key.current ??= requestKey();
230
- setTicket(await client.create(mode, { idempotencyKey: key.current }));
231
- key.current = null;
232
- } catch { setMessage('Request failed; retry the same creation.'); }
233
- finally { creating.current = false; setBusy(false); }
234
- };
163
+ if (creation.current?.mode !== mode) creation.current = { mode, key: requestKey() };
164
+ const value = await client.create(mode, { idempotencyKey: creation.current.key, signal: attempt.signal });
165
+ if (attempt.signal.aborted) return;
166
+ setTicket(value); creation.current = null;
167
+ } catch (error) { if (!attempt.signal.aborted) setMessage((error as Error).message); }
168
+ finally {
169
+ inFlight.current = false;
170
+ if (!attempt.signal.aborted) setBusy(false);
171
+ }
172
+ }
235
173
  const Component = mode === 'bind' ? AuthLinkBind : AuthLinkConfirm;
236
174
  return <>
237
- {!ticket && <button type="button" disabled={busy} onClick={() => void start()}>Start verification</button>}
238
- {ticket && <Component ticket={ticket} client={client}
239
- onConfirmed={result => result.purpose === 'bind' ? showBound(result.requestId) : showConfirmed(result.requestId)}
240
- onClose={() => finish('Closed')}
241
- onRejected={() => finish('Request rejected on the phone')}
242
- onExpired={() => finish('Request expired; start again')}
175
+ {!ticket && <button disabled={busy} onClick={() => void start()}>Start verification</button>}
176
+ {ticket && <Component ticket={ticket} client={client} variant="B"
177
+ onConfirmed={result => finish(`${result.purpose}: ${result.requestId}`)}
178
+ onClose={() => finish('Closed')} onRejected={() => finish('Rejected')}
179
+ onExpired={() => finish('Expired; start again')}
243
180
  onError={error => setMessage(error.message)} />}
244
181
  <p role="status">{message}</p>
245
182
  </>;
246
183
  }
247
184
  ```
248
185
 
249
- バックエンドは結果を検証・消費・保存してから `confirmed` を返します。`showBound` は権限付与や紐付け保存を行いません。通知、サイトのアカウント状態 API の再取得、管理ページへの移動などに置き換えられます。ページ再読み込み後はサーバーから状態を取得し、コールバックや localStorage を認可に使わないでください。作成失敗時には同じ冪等キーを再利用します。
250
-
251
-
252
- `locale` を省略すると簡体字中国語です。日本語は `locale="ja"`、簡体字中国語は `locale="zh-CN"` を指定します。client と ticket はレンダー間で安定したオブジェクトにしてください。React エントリーは `"use client"` を保持し、SSR ではリクエストを作成しません。
253
-
254
- ## Vue
255
-
256
- ```vue
257
- <script setup lang="ts">
258
- import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/vue';
259
- import type { Ticket, Confirmation } from '@auth-link/web';
260
- import { client } from './authlink-client';
261
-
262
- defineProps<{ ticket: Ticket }>();
263
- function confirmed(result: Confirmation) {
264
- console.log('Confirmed request:', result.requestId);
265
- }
266
- </script>
267
-
268
- <template>
269
- <AuthLinkBind v-if="ticket.purpose === 'bind'" :ticket="ticket" :client="client"
270
- @confirmed="confirmed" />
271
- <AuthLinkConfirm v-else :ticket="ticket" :client="client" locale="ja"
272
- @confirmed="confirmed" />
273
- </template>
274
- ```
275
-
276
- `:locale="selectedLanguage"` で表示言語を切り替えられます。言語切り替えによって新しい認証リクエストは作成されません。
277
-
278
- ## レイアウトの選択
279
-
280
- 初期化時の `variant` で選択します。省略時は **A** です。
281
-
282
- | 値 | デスクトップ | モバイル |
283
- | --- | --- | --- |
284
- | `A`(既定) | ミニマルなカードと細いコード更新バー | 中央のカード |
285
- | `B` | QR と照合コードの二列表示 | 縦並び |
286
- | `C` | コンパクトなカードとキャンセル操作 | セーフエリア対応のボトムシート |
186
+ ## 5. 通常の JavaScript / 直接表示
287
187
 
288
188
  ```ts
289
- openAuthLinkBind({ ticket, client }); // A、既定言語は簡体字中国語
290
- openAuthLinkConfirm({ ticket, client, variant: 'B' });
291
- // modal.setVariant('C');
292
- ```
293
-
294
- ```tsx
295
- <AuthLinkBind ticket={ticket} client={client} variant="C" />
296
- ```
189
+ import { openAuthLinkConfirm, requestKey } from '@auth-link/web';
190
+ import '@auth-link/web/styles.css';
297
191
 
298
- ```vue
299
- <AuthLinkConfirm :ticket="ticket" :client="client" variant="B" />
192
+ // Inside a click handler; client is the stable instance created above.
193
+ const ticket = await client.create('confirm', { idempotencyKey: requestKey() });
194
+ const modal = openAuthLinkConfirm({
195
+ ticket, client, variant: 'B', locale: 'zh-CN',
196
+ onConfirmed: result => console.log(result.requestId),
197
+ onRejected: result => console.log(result.code),
198
+ onExpired: () => console.log('Expired'),
199
+ onError: error => console.log(error.message),
200
+ onClose: () => console.log('Closed'),
201
+ });
202
+ // Route/component cleanup: modal.destroy();
203
+ // Optional: modal.close(); modal.retry(); modal.setLocale('ja'); modal.setVariant('C');
300
204
  ```
301
205
 
302
- すべてのレイアウトで細い進捗バーと「照合コードは30秒ごとに自動更新」を表示します。通常は「スマートフォンの確認待ち」を表示し、残り30秒になったときだけオレンジ色の期限警告と 00:29 形式に切り替わります。コード変更時の短いフェードはモーション低減設定に従い、リクエスト期限は延長されません。
303
-
304
- React / Vue では表示中にも変更でき、リクエストやポーリングは再作成されません。未対応の値は A に戻ります。B / C のキャンセル操作は閉じるアイコンと同じく表示の監視を停止し、サーバーのリクエストは拒否しません。確認成功時は自動で閉じます。閉じる操作のタッチ領域は 44px で、キーボードのフォーカス表示も維持します。
206
+ 登録時は client.create('bind', ...) と openAuthLinkBind(...) を使います。確認・拒否は自動で閉じてから通知し、onClose は発火しません。閉じる操作は監視を停止します。画面を離れる際は destroy を呼びます。同じ要求の成功はインスタンス内で一度だけ通知します。イベントは表示更新用で、保護操作は自サイトの業務 API を呼びます。
305
207
 
306
- ## コンポーネントの設定
208
+ ## 6. フロントエンドが使う API の形式
307
209
 
308
- | 設定 | 型 | 説明 |
210
+ | 自サイトのルート | 送信 | 受信 |
309
211
  | --- | --- | --- |
310
- | `ticket` | `Ticket`、必須 | バックエンドで作成したリクエスト |
311
- | `client` | `AuthLinkClient`、必須 | バックエンドの結果を取得するクライアント |
312
- | `locale` | `Locale` | 既定は `zh-CN` |
313
- | `variant` | `DialogVariant` | `A`(既定)、`B`、`C` |
314
- | `showQr` | `boolean` | 確認画面にも QR を表示。登録画面では常に表示 |
315
- | `intervalMs` | `number` | 250~10000 ミリ秒。既定は 1500 ミリ秒 |
316
- | `onRefresh` | `() => void` | 期限切れ後、新規作成して ticket を更新する処理 |
317
-
318
- | React コールバック / Vue イベント | 引数 | 意味 |
319
- | --- | --- | --- |
320
- | `onConfirmed` / `confirmed` | `Confirmation` | バックエンドによる検証と消費が完了 |
321
- | `onRejected` / `rejected` | `Rejection` | 拒否時はダイアログを自動で閉じる |
322
- | `onExpired` / `expired` | なし | 有効期限切れ |
323
- | `onError` / `error` | `Error` | 通信・応答検証・コールバックのエラー |
324
-
325
- 拒否時は `{ status: 'rejected', code: 'REJECTED', requestId, purpose }` を返します。`REJECTED` は SDK の業務ステータスで、HTTP ステータスや承認証明ではありません。拒否・再試行画面は表示せず、`onClose` も通知しません。メッセージや新規作成はサイト側で処理してください。React / Vue では `onRejected` / `@rejected` で ticket を解除してコンポーネントを破棄します。ヘッドレス hook の `snapshot.status` は `rejected` のままです。
212
+ | POST /api/authlink/requests | purpose=bind/confirm、Idempotency-Key | Ticket |
213
+ | POST /api/authlink/requests/{requestId}/result | 空オブジェクト、現在のログイン情報 | Result(自動監視) |
326
214
 
327
- 独自 UI には React の `useAuthLink(options)`、Vue の `useAuthLink(() => options)` を利用できます。返される値は `snapshot` と `retry` です。Vue の snapshot は shallow ref です。retry は既存リクエストを再開し、onRefresh はサイト側のコードで新規作成します。
215
+ Cookie モードでは両方に現在の CSRF ヘッダーを送ります。Ticket は requestId、purpose、qr、pairingCode、expiresAt、confirm 時は displayCode も必要です。Ticket/Result/Confirmation 型を import し、QR や数字を自作しません。confirmed のみ成功イベントを発火します。new/pending/approved/consumed は待機、rejected/expired は対応イベントです。
328
216
 
329
- ## バックエンド API
330
-
331
- 既定の接頭辞は `/api/authlink` です。両 API で Cookie + 設定した CSRF ヘッダー、または Bearer token による本人確認とリクエスト所有者を検証してください。作成時は `Idempotency-Key` も送信されます。サイトのユーザー ID は検証済みセッション/token から決定します。
217
+ create() は purpose のみ送ります。業務固有項目が必要なら独自 fetch で作成し、validateTicket で検証して同じコンポーネントへ渡します。post/session/backendOrigin は既存のフロントエンド状態で、同じ入力の再試行では同じキーを保持します。
332
218
 
333
219
  ```ts
334
- import type { Ticket, Result } from '@auth-link/web';
335
-
336
- // POST /api/authlink/requests
337
- // Body: { purpose: 'bind' | 'confirm' }
338
- // Response: Ticket
339
-
340
- // POST /api/authlink/requests/{requestId}/result
341
- // Body: {}
342
- // Response: Result
343
- ```
344
-
345
- ```json
346
- {
347
- "status": "confirmed",
348
- "requestId": "<request-id>",
349
- "verificationId": "<verification-id>",
350
- "purpose": "confirm",
351
- "authenticationPolicy": "device-presence.v1",
352
- "expiresAt": "<same-expiry-as-ticket>"
353
- }
220
+ import { requestKey, validateTicket } from '@auth-link/web';
221
+
222
+ // Keep this key across retries of the SAME operation and payload.
223
+ const idempotencyKey = requestKey();
224
+ const response = await fetch(`${backendOrigin}/api/authlink/requests`, {
225
+ method: 'POST', credentials: 'include', cache: 'no-store', redirect: 'error',
226
+ headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': session.csrf,
227
+ 'Idempotency-Key': idempotencyKey },
228
+ body: JSON.stringify({ purpose: 'confirm', postId: post.id, version: post.version }),
229
+ });
230
+ if (!response.ok) throw new Error('Creation failed');
231
+ const ticket = validateTicket(await response.json(), 'confirm', 'https://api.getauthlink.com');
232
+ // Pass ticket and the same client to AuthLinkConfirm.
354
233
  ```
355
234
 
356
- 作成 API は AuthLink の公開 ticket(`requestId`、`purpose`、`qr`、`pairingCode`、`expiresAt`、現在の `displayCode`)を返します。displayCode は `value`、`serverTime`、`validUntil`、`periodSeconds: 30` を持ちます。確認用 ticket には displayCode が必要です。
357
-
358
- 結果 API は未完了状態と最新の表示コードを返します。`new` / `pending` / `approved` / `consumed` だけでは成功イベントは発火しません。`confirmed` を返す前に、バックエンドは保存した state と消費用冪等キーで AuthLink の結果を消費し、完全な受領情報を検証して正しいセッションに記録します。requestId / verificationId による永続的な冪等処理が必要です。
359
-
360
- ## 対応言語
235
+ ## 7. 設定・イベント・表示
361
236
 
362
- | Code | Language |
363
- | --- | --- |
364
- | `en` | English |
365
- | `zh-CN` | 简体中文 (default) |
366
- | `zh-TW` | 繁體中文 |
367
- | `ja` | 日本語 |
368
- | `bn` | বাংলা |
369
- | `es` | Español |
370
- | `fil` | Filipino |
371
- | `hi` | हिन्दी |
372
- | `id` | Bahasa Indonesia |
373
- | `ms` | Bahasa Melayu |
374
- | `pt` | Português |
375
- | `th` | ไทย |
376
- | `vi` | Tiếng Việt |
237
+ | prop | 型 | 用途 |
238
+ | --- | --- | --- |
239
+ | `ticket` | `Ticket` | Required; the current request |
240
+ | `client` | `AuthLinkClient` | Required; keep the instance stable |
241
+ | `locale` | `Locale` | Default `zh-CN` |
242
+ | `variant` | `'A' / 'B' / 'C'` | Default `A` |
243
+ | `showQr` | `boolean` | Optional QR for confirm; bind always shows QR |
244
+ | `intervalMs` | `number` | Default 1500; range 250–10000 ms |
245
+ | `onRefresh` | `() => void` | Optional user action after expiry; create a new ticket |
246
+ | `className` | `string` | Custom dialog class; Vue also accepts `class` |
247
+
248
+ | React コールバック / Vue イベント | 値 | 動作 |
249
+ | --- | --- | --- |
250
+ | `onConfirmed` / `confirmed` | `Confirmation` | Success; dialog closes automatically |
251
+ | `onRejected` / `rejected` | `Rejection` | `{status:'rejected',code:'REJECTED',requestId,purpose}`; dialog closes |
252
+ | `onExpired` / `expired` | — | Request expired |
253
+ | `onClose` / `close` | — | User closes the dialog; clear ticket |
254
+ | `onError` / `error` | `Error` | Read `error.message`; retain the dialog for retry |
377
255
 
378
- 未指定・未対応の言語コードは簡体字中国語にフォールバックします。ルート要素には `lang` が設定されます。ルートエントリーの `supportedLocales` で一覧を取得できます。言語変更は表示文言だけに作用し、数字やリクエストは変更しません。ドキュメントは英語・中国語・日本語で提供します。
256
+ A は中央カード(既定)、B はデスクトップ分割・狭い画面で縦配置、C は小型・モバイルで下部シートです。locale/variant の変更では要求を作り直しません。ネイティブ dialog.showModal() 対応ブラウザーが必要です。
379
257
 
380
- ## スタイルとエントリー
258
+ CSS を明示 import します。ダイアログは document.body 配下なので :root または .authlink-dialog で --authlink-foreground、--authlink-background、--authlink-accent、--authlink-muted を指定します。Vue class は文字列・配列・オブジェクトを className と併用できます。SSR はダイアログや要求を作りません。
381
259
 
382
- `@auth-link/web/styles.css` を明示的に読み込みます。ダイアログは `document.body` に追加されるため、`:root` または `.authlink-dialog` で `--authlink-foreground`、`--authlink-background`、`--authlink-accent`、`--authlink-muted` を上書きします。クラス名の接頭辞は `authlink-` です。
260
+ 言語:`zh-CN` (default), `zh-TW`, `en`, `ja`, `bn`, `es`, `fil`, `hi`, `id`, `ms`, `pt`, `th`, `vi`。未指定・未対応値は簡体字中国語へフォールバックします。supportedLocales で一覧を取得できます。
383
261
 
384
- React / Vue はダイアログの `className` に対応します。Vue は文字列・配列・オブジェクト形式の `class` も受け取り、`className` と結合します。再作成ボタンは親が渡した最新の `onRefresh` を呼び出します。
262
+ ## 8. 通信設定とエラー確認
385
263
 
386
- | エントリー | 内容 |
264
+ | 設定 | 用途 |
387
265
  | --- | --- |
388
- | `@auth-link/web` | クライアント、冪等キー、対応言語、型、基本 API |
389
- | `@auth-link/web/react` | React コンポーネントと hook |
390
- | `@auth-link/web/vue` | Vue コンポーネントと composable |
391
- | `@auth-link/web/styles.css` | スタイル |
266
+ | `baseUrl` | Your application API prefix; default `/api/authlink` |
267
+ | `allowedBackendOrigins` | Exact extra application API origins; no paths/wildcards |
268
+ | `credentials` | Default `same-origin`; cross-origin cookies need `include` |
269
+ | `csrfToken` | Sync/async getter for the current website CSRF token |
270
+ | `csrfHeaderName` | Default `X-CSRF-Token` |
271
+ | `accessToken` | Sync/async website Bearer token getter, without `Bearer ` |
272
+ | `headers` | Extra headers or sync/async getter |
273
+ | `authLinkOrigin` | `https://api.getauthlink.com` for production AuthLink |
274
+ | `fetch` | Optional fetch implementation |
392
275
 
393
- ## セキュリティと有効期限
276
+ headers は Accept、Content-Type、Authorization、Idempotency-Key、設定済み CSRF ヘッダーを上書きできません。error.message を確認します。CSRF_REQUIRED は session token、ACCESS_TOKEN_REQUIRED はログイン状態、BACKEND_ORIGIN_NOT_ALLOWED は正確な許可 origin、NETWORK_ERROR は開発者ツールの Network・通信・CORS を確認します。タイムアウトは 10 秒、閉じる/アンマウントで監視を中止します。
394
277
 
395
- - 数字はアプリとの目視照合用です。数字だけで認証や承認はできません。
396
- - ブラウザーに生成 seed、端末秘密鍵、テナント秘密情報を渡しません。
397
- - リクエストは最大 120 秒。コード更新で期限は延長されません。サービスとアプリはモバイルプロトコル 7 に対応する必要があります。
398
- - サーバー時刻と単調増加時計で残り時間を計算し、通信が止まっても期限切れの数字は隠します。更新境界で短い待ち時間が生じる場合があります。
399
- - アンマウントや ticket 変更で監視を中止します。インスタンス内のイベント重複防止とは別に、バックエンドで永続的な冪等処理が必要です。
400
- - フロントエンドのイベントは UI 更新用です。保護 API はバックエンドの信頼できる記録で判断します。
401
- - QR はローカル生成し、バックエンドの SVG を挿入しません。CSP は `img-src 'self' data:` を許可してください。
278
+ 期限切れの数字は隠れます。数字をフロントエンドで生成・キャッシュしません。QR はローカル生成で、CSP 使用時は data: 画像を許可します。イベントや localStorage、ブラウザーからの状態値は業務上の許可証ではありません。
402
279
 
403
280
  ## ライセンス
404
281
 
405
- MIT。ライセンス全文は `LICENSE`、同梱ライブラリの帰属表示は `THIRD_PARTY_NOTICES.md` を参照してください。
282
+ MIT。LICENSE と THIRD_PARTY_NOTICES.md を参照してください。