@auth-link/web 0.0.1 → 0.0.3

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.en.md CHANGED
@@ -4,6 +4,12 @@
4
4
 
5
5
  Responsive dialogs for **AuthLink phone binding and mobile confirmation**, with direct function calls and React / Vue components. Display binding QR codes, synchronized six-digit matching codes, countdowns and confirmation results in any web application. No particular site platform is required.
6
6
 
7
+ ## Go backend + Vue walkthrough
8
+
9
+ Start with [Go + Vue: login, binding and confirmed publication](./go-vue.en.md) and the [complete Vue CMS example](../examples/vue-cms.vue).
10
+
11
+ The flow is site login → Go `/api/session` returns authenticated/csrf → configure the cross-origin client with `csrfToken: () => session.csrf` → create bind/confirm → Go consumes, validates and commits the receipt → Vue dismisses the dialog → a separate business API checks the saved receipt and publishes. `baseUrl` targets Go; `authLinkOrigin` identifies AuthLink. Tenant keys stay in Go. Use your own fetch plus `validateTicket` for post/version fields; SDK create() sends purpose only.
12
+
7
13
  ## Features
8
14
 
9
15
  - Phone-binding QR codes generated locally in the browser.
@@ -26,41 +32,148 @@ Compatibility: React 18.2+ / 19, Vue 3.3+, TypeScript, ESM. Install only the fra
26
32
  ## Browser integration
27
33
 
28
34
  1. Configure your backend's AuthLink tenant credentials.
29
- 2. Implement the two same-origin endpoints below, authenticated to the current website user.
35
+ 2. Implement the two application backend endpoints below (same-origin or explicitly allowed cross-origin), authenticated to the current website user.
30
36
  3. Create a request on a user action and pass its ticket to a component.
31
37
  4. Your backend verifies and consumes the result before returning a confirmed response.
32
38
 
33
- The browser can call this library directly. Tenant credentials and result consumption belong on your backend; never embed a tenant API key in frontend code. The supplied client intentionally uses a same-origin backend. You can supply a custom `AuthLinkClient` to integrate another transport while preserving server-side credential handling and verification.
39
+ The browser can call this library directly. Tenant credentials and result consumption belong on your backend; never embed a tenant API key in frontend code. The supplied client defaults to same-origin and also supports explicitly allowed cross-origin backends. You can supply a custom `AuthLinkClient` to integrate another transport while preserving server-side credential handling and verification.
40
+
41
+ ## Create a client (default same-origin session mode)
42
+
43
+ `readCsrfTokenFromYourCms` is application code, not an SDK export. It reads a CSRF token issued by your own backend for the current logged-in session; it must not generate one in the browser or return an AuthLink API key. The name does not require a CMS.
44
+
45
+ Reuse your framework's existing CSRF mechanism. Otherwise, generate a cryptographically random token on the server (for example, Node's `randomBytes(32).toString('base64url')`), persist it in the session, and render it into the authenticated page with HTML attribute escaping. `{{ csrfToken }}` below is a server-template variable, not a literal value. Do not publicly cache pages containing session tokens.
34
46
 
35
- ## Create a client
47
+ ```html
48
+ <meta name="csrf-token" content="{{ csrfToken }}">
49
+ ```
50
+
51
+ The SDK reads the token for every request, sends it as `X-CSRF-Token`, and includes same-origin session cookies. Your backend must validate that header against the token in the same session on both creation and result routes, reject missing/mismatched tokens, and check the expected request origin. Sending a header alone does not provide CSRF protection. Obtain a fresh token after login/session changes; a SPA may supply a token returned by its own same-origin session bootstrap endpoint. If your framework expects another header name, set `csrfHeaderName` or provide a custom `AuthLinkClient`.
52
+
53
+ Create `authlink-client.ts`:
36
54
 
37
55
  ```ts
38
56
  // authlink-client.ts
39
- import { createAuthLinkClient, requestKey } from '@auth-link/web';
57
+ import { createAuthLinkClient } from '@auth-link/web';
40
58
  import '@auth-link/web/styles.css';
41
59
 
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
+
42
68
  export const client = createAuthLinkClient({
43
69
  baseUrl: '/api/authlink',
44
- csrfToken: () => {
45
- const token = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content;
46
- if (!token) throw new Error('Missing CSRF token');
47
- return token;
48
- },
70
+ csrfToken: readCsrfTokenFromYourCms,
71
+ // Replace this example with the configured AuthLink service origin.
49
72
  authLinkOrigin: 'https://auth.example.com',
50
73
  });
74
+ ```
51
75
 
52
- // Call from a user action. Keep this key if you retry the same creation.
53
- const idempotencyKey = requestKey();
54
- const ticket = await client.create('bind', { idempotencyKey });
55
- // For a mobile confirmation, use client.create('confirm', { idempotencyKey }).
76
+ Replace `https://auth.example.com` with the configured AuthLink origin. Create requests only in user event handlers, as in the React example below.
77
+
78
+ ## Separate frontend and backend origins (since 0.0.2)
79
+
80
+ The default is still same-origin `/api/authlink`. For direct cross-origin requests, configure an absolute `baseUrl` and exact `allowedBackendOrigins`, for example frontend `https://www.example.com` and application API `https://api.example.com`. Subdomains and different ports are different origins. Entries must be origins only, without paths, wildcards or embedded credentials, and must come from trusted configuration. HTTPS is required except for explicitly allowed HTTP loopback development origins (localhost, 127.0.0.1, [::1]).
81
+
82
+ The SDK validates the destination before evaluating credentials and refuses redirects. `authLinkOrigin` separately validates the AuthLink service origin encoded in the QR; it is not the application API allowlist.
83
+
84
+ ### Cross-origin session cookies
85
+
86
+ This complete initialization function uses an example `GET /api/session` endpoint that **your application implements**, returning `{ "csrfToken": "current-session-token" }` for the authenticated session with `Cache-Control: no-store` and CORS enabled. Replace that path with your existing session endpoint.
87
+
88
+ `csrfToken` is the JSON property chosen for this generic example. The Go CMS walkthrough uses `{ authenticated: true, csrf: '…' }` instead; map session.csrf to the SDK csrfToken callback without renaming your backend response.
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',
97
+ });
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
+ }
56
116
  ```
57
117
 
58
- Provide a real CSRF token from your application's session in the meta element, and replace the example AuthLink origin with your configured HTTPS origin. Request creation belongs in a click handler, not a mount effect. Components only observe existing requests.
118
+ After login, call `await createSessionBackendClient('https://api.example.com')` and pass the returned `client` to the existing components. Call the returned `updateCsrfToken(newToken)` when your session refreshes its CSRF token. Close old dialogs and recreate session state on logout/account changes. Cross-origin cookies require `credentials: 'include'`, but their SameSite, Secure, domain/path and browser third-party cookie restrictions still apply. Cross-origin does not necessarily mean cross-site. Truly cross-site cookies typically require `SameSite=None; Secure`, and browser policy may still block them.
119
+
120
+ ### Cross-origin Bearer / JWT
121
+
122
+ Pass your website's existing mutable login state. Its login/refresh flow updates `auth.accessToken`; the callback reads it on every creation and poll.
123
+
124
+ ```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
+ });
135
+ }
136
+ ```
137
+
138
+ This must be the website user's login token, never an AuthLink tenant API key. The SDK adds `Authorization: Bearer ...`. With `accessToken`, credentials default to `omit` and CSRF can be omitted for a backend that exclusively authenticates Bearer tokens without cookie fallback. Explicit `include` or `same-origin` credentials require a CSRF provider even with Bearer authentication.
139
+
140
+ ### Backend CORS
141
+
142
+ Handle `OPTIONS` preflight and return appropriate CORS headers on preflight, actual responses and errors, including the session bootstrap endpoint. For a frontend at `https://www.example.com`:
143
+
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
150
+ ```
151
+
152
+ Credentialed CORS cannot use `Access-Control-Allow-Origin: *`. Match a server-side origin allowlist before returning the current allowed origin; do not reflect arbitrary origins. Preflight generally has no login credentials: validate its origin/method/headers, then authenticate the actual request and validate CSRF for cookies and request ownership. Add custom CSRF/extra headers to `Access-Control-Allow-Headers`. The SDK cannot enable backend CORS or read results using `no-cors`. See [MDN CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS).
153
+
154
+ ### Transport options
155
+
156
+ | Option | Behavior |
157
+ | --- | --- |
158
+ | `baseUrl` | Application API prefix, defaults to `/api/authlink`; absolute for cross-origin |
159
+ | `allowedBackendOrigins` | Additional exact application backend origins; same-origin remains allowed |
160
+ | `credentials` | Defaults to `same-origin`, or `omit` with `accessToken`; use `include` for cross-origin cookies |
161
+ | `csrfToken` | Per-request sync/async token callback, required for cookie/session mode |
162
+ | `csrfHeaderName` | Defaults to `X-CSRF-Token`, configurable for framework integration |
163
+ | `accessToken` | Per-request sync/async website login token callback, without the `Bearer ` prefix |
164
+ | `headers` | Extra `HeadersInit`, or sync/async callback returning it |
165
+ | `authLinkOrigin` | Trusted QR service origin, independent of the application API origin |
166
+ | `fetch` | Optional implementation; destination validation still applies |
167
+
168
+ Custom headers cannot override SDK-managed `Accept`, `Content-Type`, `Authorization`, `Idempotency-Key` or the configured CSRF header (`RESERVED_HEADER`). Use `accessToken` for Bearer auth and `csrfHeaderName` to adapt framework CSRF validation. Import `ClientOptions` from the root entry for configuration types.
169
+
170
+ Unlisted origins produce `BACKEND_ORIGIN_NOT_ALLOWED` (replacing `CMS_ORIGIN_REQUIRED`); invalid allowlist entries or URLs produce `INVALID_BACKEND_ORIGIN` / `INVALID_BASE_URL`. Missing login/CSRF tokens produce `ACCESS_TOKEN_REQUIRED` / `CSRF_REQUIRED`. Cancellation and the 10-second timeout also cover async credential providers. Browser CORS failures normally surface as `NETWORK_ERROR`; inspect browser developer tools for details.
59
171
 
60
172
  ## Open a dialog directly (React, Vue or plain JavaScript)
61
173
 
62
174
  ```ts
63
175
  import { openAuthLinkBind, openAuthLinkConfirm, requestKey } from '@auth-link/web';
176
+ import { client } from './authlink-client';
64
177
  import '@auth-link/web/styles.css';
65
178
 
66
179
  // Call in your button handler, after your backend creates the request.
@@ -79,22 +192,62 @@ The dialog adapts to mobile widths, has a scrollable height, keyboard Escape and
79
192
 
80
193
  ## React
81
194
 
195
+ `showBound(requestId)` is application UI code: it sets a message and clears the ticket to unmount the dialog. The example also defines all rejection, expiry and close handlers. Use `<AuthLinkPanel mode="bind" />` or `<AuthLinkPanel mode="confirm" />`.
196
+
82
197
  ```tsx
198
+ "use client";
199
+ import { useRef, useState } from 'react';
200
+ import { requestKey } from '@auth-link/web';
201
+ import type { Mode, Ticket } from '@auth-link/web';
83
202
  import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/react';
84
- import type { Ticket, Confirmation } from '@auth-link/web';
85
203
  import { client } from './authlink-client';
86
204
 
87
- export function VerificationPanel({ ticket }: { ticket: Ticket }) {
88
- const onConfirmed = (result: Confirmation) => {
89
- console.log('Confirmed request:', result.requestId);
205
+ export function AuthLinkPanel({ mode }: { mode: Mode }) {
206
+ const [ticket, setTicket] = useState<Ticket | null>(null);
207
+ const [busy, setBusy] = useState(false);
208
+ const [message, setMessage] = useState('');
209
+ const key = useRef<string | null>(null);
210
+ const creating = useRef(false);
211
+ function finish(message: string) {
212
+ setTicket(null);
213
+ setMessage(message);
214
+ }
215
+ function showBound(requestId: string) {
216
+ // UI feedback only. The backend has already verified and saved the binding.
217
+ finish(`Phone bound (request ${requestId})`);
218
+ }
219
+ function showConfirmed(requestId: string) {
220
+ finish(`Confirmed (request ${requestId})`);
221
+ }
222
+ const start = async () => {
223
+ if (creating.current || ticket) return;
224
+ creating.current = true;
225
+ setBusy(true);
226
+ setMessage('');
227
+ try {
228
+ key.current ??= requestKey();
229
+ setTicket(await client.create(mode, { idempotencyKey: key.current }));
230
+ key.current = null;
231
+ } catch { setMessage('Request failed; retry the same creation.'); }
232
+ finally { creating.current = false; setBusy(false); }
90
233
  };
91
- return ticket.purpose === 'bind'
92
- ? <AuthLinkBind ticket={ticket} client={client} onConfirmed={onConfirmed} />
93
- : <AuthLinkConfirm ticket={ticket} client={client} locale="en"
94
- onConfirmed={onConfirmed} onExpired={() => console.log('Expired')} />;
234
+ const Component = mode === 'bind' ? AuthLinkBind : AuthLinkConfirm;
235
+ return <>
236
+ {!ticket && <button type="button" disabled={busy} onClick={() => void start()}>Start verification</button>}
237
+ {ticket && <Component ticket={ticket} client={client}
238
+ onConfirmed={result => result.purpose === 'bind' ? showBound(result.requestId) : showConfirmed(result.requestId)}
239
+ onClose={() => finish('Closed')}
240
+ onRejected={() => finish('Request rejected on the phone')}
241
+ onExpired={() => finish('Request expired; start again')}
242
+ onError={error => setMessage(error.message)} />}
243
+ <p role="status">{message}</p>
244
+ </>;
95
245
  }
96
246
  ```
97
247
 
248
+ The backend must verify, consume and persist the binding/confirmation before returning `confirmed`. `showBound` does not grant access or save a binding. You can instead show a toast, reload your own account-status API or navigate to a management page. Reload real binding state from the backend after a page refresh; never treat a callback, local state or localStorage as authorization. The example retains the same creation idempotency key after a failed request.
249
+
250
+
98
251
  Mounting either component opens a dialog; unmount it in `onClose` (for example, clear the selected ticket). SSR renders no dialog and makes no requests. Omit `locale` for Simplified Chinese. Use `locale="zh-CN"` for Simplified Chinese or another code from the language table. Keep `client` and `ticket` stable between renders. The React entry preserves `"use client"`; SSR rendering does not create requests.
99
252
 
100
253
  ## Vue
@@ -175,7 +328,7 @@ Rejection returns `{ status: 'rejected', code: 'REJECTED', requestId, purpose }`
175
328
 
176
329
  ## Backend contract
177
330
 
178
- The default prefix is `/api/authlink`. Both endpoints use your website session and `X-CSRF-Token`. Creation also sends a stable `Idempotency-Key`. Your backend derives the website account from the session rather than a browser-supplied account ID.
331
+ The default prefix is `/api/authlink`. Both endpoints authenticate the website user using session cookies plus the configured CSRF header, or Bearer authentication. Creation also sends a stable `Idempotency-Key`. Derive the account from the validated session/token rather than a browser-supplied account ID.
179
332
 
180
333
  ```ts
181
334
  import type { Ticket, Result } from '@auth-link/web';
@@ -202,7 +355,7 @@ import type { Ticket, Result } from '@auth-link/web';
202
355
 
203
356
  Creation returns the public ticket from AuthLink: `requestId`, `purpose`, `qr`, `pairingCode`, `expiresAt` and the current `displayCode`. The display frame contains `value`, `serverTime`, `validUntil`, `periodSeconds: 30`. A confirmation ticket requires a display frame.
204
357
 
205
- The result endpoint verifies session/request ownership and forwards pending states and the current display frame. States `new`, `pending`, `approved` and `consumed` alone do not trigger success. Before returning `confirmed`, your backend must consume the AuthLink result with its saved state and stable consume idempotency key, check the complete receipt and persist confirmation for the correct session. Persist idempotence using request/verification IDs.
358
+ The result endpoint verifies the authenticated user and request ownership (plus CSRF for cookie mode) and forwards pending states and the current display frame. States `new`, `pending`, `approved` and `consumed` alone do not trigger success. Before returning `confirmed`, your backend must consume the AuthLink result with its saved state and stable consume idempotency key, check the complete receipt and persist confirmation for the correct session. Persist idempotence using request/verification IDs.
206
359
 
207
360
  ## Languages
208
361
 
@@ -228,6 +381,8 @@ Missing or unsupported locale codes fall back to Simplified Chinese. The panel s
228
381
 
229
382
  Explicitly import `@auth-link/web/styles.css`. Override `--authlink-foreground`, `--authlink-background`, `--authlink-accent` and `--authlink-muted` on `:root` or `.authlink-dialog`; dialogs are mounted under `document.body`. All component classes use the `authlink-` prefix.
230
383
 
384
+ React and Vue accept `className` for the dialog. Vue also accepts standard string, array or object `class` attributes, combined with `className`. Refresh buttons use the latest `onRefresh` callback supplied by the parent.
385
+
231
386
  | Import | Contents |
232
387
  | --- | --- |
233
388
  | `@auth-link/web` | Client, request key, supported locales, types and core tools |
@@ -247,4 +402,4 @@ Explicitly import `@auth-link/web/styles.css`. Override `--authlink-foreground`,
247
402
 
248
403
  ## License
249
404
 
250
- UNLICENSED. Public availability of the package does not grant an open-source license. Bundled third-party attribution is in `THIRD_PARTY_NOTICES.md`.
405
+ MIT. See `LICENSE` for the full license text and `THIRD_PARTY_NOTICES.md` for bundled third-party attribution.
package/docs/README.ja.md CHANGED
@@ -4,26 +4,11 @@
4
4
 
5
5
  **AuthLink のスマートフォン登録とアプリでの確認**を Web サイトに組み込む React / Vue コンポーネントです。登録用 QR コード、同期する 6 桁の照合コード、カウントダウン、確認結果を表示します。特定のサイト基盤には依存しません。
6
6
 
7
- ## 関数でダイアログを開く(React / Vue / JavaScript)
7
+ ## Go + Vue の接続手順
8
8
 
9
- ```ts
10
- import { openAuthLinkBind, openAuthLinkConfirm, requestKey } from '@auth-link/web';
11
- import '@auth-link/web/styles.css';
12
-
13
- const ticket = await client.create('confirm', { idempotencyKey: requestKey() });
14
- const modal = openAuthLinkConfirm({
15
- ticket, client, locale: 'ja',
16
- onConfirmed: result => console.log(result.requestId),
17
- onClose: () => console.log('Closed'),
18
- });
19
- // 登録: openAuthLinkBind({ ticket: bindingTicket, client })
20
- // modal.close(); modal.setLocale('en');
21
- ```
22
-
23
- React / Vue コンポーネントもマウント時にダイアログを開きます。React の onClose、Vue の @close で ticket を消去してアンマウントしてください。SSR はダイアログも通信も生成しません。スマートフォンの画面幅、スクロール、閉じるボタン、Escape に対応し、ネイティブ dialog.showModal() が必要です。body 直下に表示するため、テーマ変数は :root または .authlink-dialog に設定します。
24
-
25
- 閉じてもサーバーのリクエストを拒否・取消しません。元の有効期限は維持されます。承認・拒否時は自動で閉じ、onConfirmed または onRejected を通知します。ユーザー操作の onClose は通知しません。関数で開いた場合はページ破棄時に modal.destroy() を呼んで通信と UI を解除してください。destroy() は onClose を通知しません。modal.setLocale() はリクエストを作り直さずに言語を切り替えます。
9
+ Go + Vue の実際の接続手順は [ログイン、端末登録、確認後の公開](./go-vue.ja.md)、完全なコンポーネントは [Vue CMS 例](../examples/vue-cms.vue) を参照してください。
26
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 のみ送ります。
27
12
 
28
13
  ## 主な機能
29
14
 
@@ -47,55 +32,223 @@ npm install @auth-link/web
47
32
  ## 接続の流れ
48
33
 
49
34
  1. サイトのバックエンドに AuthLink のテナント資格情報を設定します。
50
- 2. 下記の同一オリジンの作成・結果取得 API を実装します。
35
+ 2. 下記の業務バックエンド(同一オリジンまたは明示的に許可した別オリジン)の作成・結果取得 API を実装します。
51
36
  3. ユーザー操作でリクエストを作成し、取得した ticket をコンポーネントに渡します。
52
37
  4. バックエンドが結果を検証・消費した後に確認完了を返します。
53
38
 
54
- ブラウザーからこのライブラリを呼び出せますが、テナント API キーと結果の消費処理は必ずサーバー側に置きます。標準クライアントは同一オリジンのバックエンドに接続します。独自の通信方式では `AuthLinkClient` を実装できますが、資格情報と検証をブラウザーに移さないでください。
39
+ ブラウザーからこのライブラリを呼び出せますが、テナント API キーと結果の消費処理は必ずサーバー側に置きます。標準クライアントは既定の同一オリジンと、明示的に許可した別オリジンに対応します。独自の通信方式では `AuthLinkClient` を実装できますが、資格情報と検証をブラウザーに移さないでください。
40
+
41
+ ## クライアント作成(既定の同一オリジン Cookie モード)
55
42
 
56
- ## クライアント作成
43
+ `readCsrfTokenFromYourCms` は SDK のエクスポートではなく、接続先サイトで実装する関数です。現在のログインセッションに対してバックエンドが発行した CSRF token を読み取ります。CMS は必須ではありません。ブラウザーで値を生成したり、AuthLink の API キーを返したりしないでください。
44
+
45
+ 既存フレームワークの CSRF 機構を優先してください。独自に実装する場合は、サーバーで暗号学的乱数(Node の `randomBytes(32).toString('base64url')` など)を生成し、セッションに保存して HTML 属性をエスケープしたうえでページに埋め込みます。下記の `{{ csrfToken }}` はサーバーテンプレートの変数です。このページを公開キャッシュしないでください。
46
+
47
+ ```html
48
+ <meta name="csrf-token" content="{{ csrfToken }}">
49
+ ```
50
+
51
+ SDK は各リクエストで token を読み取り、`X-CSRF-Token` と同一オリジンのセッション cookie を送信します。作成 API と結果 API の両方で、バックエンドが同じセッションの token と照合し、欠落・不一致や不正なリクエスト元を拒否する必要があります。ログインやセッション変更後は最新 token を取得してください。SPA ではサイト独自の同一オリジン初期化 API から取得する方法も使えます。別のヘッダー名を使うフレームワークでは`csrfHeaderName` または独自 `AuthLinkClient` で適合します。
52
+
53
+ `authlink-client.ts` を作成します:
57
54
 
58
55
  ```ts
59
56
  // authlink-client.ts
60
- import { createAuthLinkClient, requestKey } from '@auth-link/web';
57
+ import { createAuthLinkClient } from '@auth-link/web';
61
58
  import '@auth-link/web/styles.css';
62
59
 
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
+
63
68
  export const client = createAuthLinkClient({
64
69
  baseUrl: '/api/authlink',
65
- csrfToken: () => {
66
- const token = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content;
67
- if (!token) throw new Error('Missing CSRF token');
68
- return token;
69
- },
70
+ csrfToken: readCsrfTokenFromYourCms,
71
+ // Replace this example with the configured AuthLink service origin.
70
72
  authLinkOrigin: 'https://auth.example.com',
71
73
  });
74
+ ```
75
+
76
+ `https://auth.example.com` は実際の AuthLink オリジンに置き換えてください。リクエストは下記の 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 の許可リストとは別です。
72
83
 
73
- // Call from a user action. Keep this key if you retry the same creation.
74
- const idempotencyKey = requestKey();
75
- const ticket = await client.create('bind', { idempotencyKey });
76
- // For a mobile confirmation, use client.create('confirm', { idempotencyKey }).
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',
97
+ });
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
+ }
77
116
  ```
78
117
 
79
- meta 要素にサイトのセッションに対応する CSRF トークンを設定し、AuthLink の HTTPS オリジンを実際の設定に置き換えてください。作成処理はクリックハンドラーで実行し、マウント時の effect では実行しません。コンポーネントは既存のリクエストを監視します。同じ作成操作を再試行する際は同じ冪等キーを使います。
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 は作成・ポーリングのたびに最新値を読み取ります。
123
+
124
+ ```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
+ });
135
+ }
136
+ ```
137
+
138
+ これはサイト利用者のログイン token であり、AuthLink のテナント API キーではありません。SDK が `Authorization: Bearer ...` を付加します。`accessToken` 指定時は既定で `credentials: 'omit'` となり、Cookie にフォールバックせず Bearer のみで認証するバックエンドでは CSRF を省略できます。`include` / `same-origin` を明示して Cookie を併用する場合は CSRF の提供・検証も必要です。
139
+
140
+ ### バックエンドの CORS
141
+
142
+ `OPTIONS` プリフライトを処理し、実リクエスト・エラー応答・session 初期化 API にも必要な CORS ヘッダーを返します。フロントエンドが `https://www.example.com` の例:
143
+
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
150
+ ```
151
+
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
+ ### クライアント設定
155
+
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 実装;接続先検証は維持 |
167
+
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';
177
+ import '@auth-link/web/styles.css';
178
+
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 に設定します。
190
+
191
+ 閉じてもサーバーのリクエストを拒否・取消しません。元の有効期限は維持されます。承認・拒否時は自動で閉じ、onConfirmed または onRejected を通知します。ユーザー操作の onClose は通知しません。関数で開いた場合はページ破棄時に modal.destroy() を呼んで通信と UI を解除してください。destroy() は onClose を通知しません。modal.setLocale() はリクエストを作り直さずに言語を切り替えます。
192
+
80
193
 
81
194
  ## React
82
195
 
196
+ `showBound(requestId)` はサイトの表示更新用関数です。メッセージを更新し、ticket を消去してダイアログをアンマウントします。下記には拒否・期限切れ・閉じる処理も含まれます。`<AuthLinkPanel mode="bind" />` または `<AuthLinkPanel mode="confirm" />` で使います。
197
+
83
198
  ```tsx
199
+ "use client";
200
+ import { useRef, useState } from 'react';
201
+ import { requestKey } from '@auth-link/web';
202
+ import type { Mode, Ticket } from '@auth-link/web';
84
203
  import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/react';
85
- import type { Ticket, Confirmation } from '@auth-link/web';
86
204
  import { client } from './authlink-client';
87
205
 
88
- export function VerificationPanel({ ticket }: { ticket: Ticket }) {
89
- const onConfirmed = (result: Confirmation) => {
90
- console.log('Confirmed request:', result.requestId);
206
+ export function AuthLinkPanel({ mode }: { mode: Mode }) {
207
+ const [ticket, setTicket] = useState<Ticket | null>(null);
208
+ const [busy, setBusy] = useState(false);
209
+ 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('');
228
+ 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); }
91
234
  };
92
- return ticket.purpose === 'bind'
93
- ? <AuthLinkBind ticket={ticket} client={client} onConfirmed={onConfirmed} />
94
- : <AuthLinkConfirm ticket={ticket} client={client} locale="en"
95
- onConfirmed={onConfirmed} onExpired={() => console.log('Expired')} />;
235
+ const Component = mode === 'bind' ? AuthLinkBind : AuthLinkConfirm;
236
+ 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')}
243
+ onError={error => setMessage(error.message)} />}
244
+ <p role="status">{message}</p>
245
+ </>;
96
246
  }
97
247
  ```
98
248
 
249
+ バックエンドは結果を検証・消費・保存してから `confirmed` を返します。`showBound` は権限付与や紐付け保存を行いません。通知、サイトのアカウント状態 API の再取得、管理ページへの移動などに置き換えられます。ページ再読み込み後はサーバーから状態を取得し、コールバックや localStorage を認可に使わないでください。作成失敗時には同じ冪等キーを再利用します。
250
+
251
+
99
252
  `locale` を省略すると簡体字中国語です。日本語は `locale="ja"`、簡体字中国語は `locale="zh-CN"` を指定します。client と ticket はレンダー間で安定したオブジェクトにしてください。React エントリーは `"use client"` を保持し、SSR ではリクエストを作成しません。
100
253
 
101
254
  ## Vue
@@ -175,7 +328,7 @@ React / Vue では表示中にも変更でき、リクエストやポーリン
175
328
 
176
329
  ## バックエンド API
177
330
 
178
- 既定の接頭辞は `/api/authlink` です。両 API でサイトのセッション、`X-CSRF-Token`、リクエストの所有者を検証してください。作成時は `Idempotency-Key` も送信されます。サイトのユーザー ID はセッションから決定します。
331
+ 既定の接頭辞は `/api/authlink` です。両 API で Cookie + 設定した CSRF ヘッダー、または Bearer token による本人確認とリクエスト所有者を検証してください。作成時は `Idempotency-Key` も送信されます。サイトのユーザー ID は検証済みセッション/token から決定します。
179
332
 
180
333
  ```ts
181
334
  import type { Ticket, Result } from '@auth-link/web';
@@ -226,7 +379,9 @@ import type { Ticket, Result } from '@auth-link/web';
226
379
 
227
380
  ## スタイルとエントリー
228
381
 
229
- `@auth-link/web/styles.css` を明示的に読み込みます。親要素で `--authlink-foreground`、`--authlink-background`、`--authlink-accent`、`--authlink-muted` を上書きできます。クラス名の接頭辞は `authlink-` です。
382
+ `@auth-link/web/styles.css` を明示的に読み込みます。ダイアログは `document.body` に追加されるため、`:root` または `.authlink-dialog` で `--authlink-foreground`、`--authlink-background`、`--authlink-accent`、`--authlink-muted` を上書きします。クラス名の接頭辞は `authlink-` です。
383
+
384
+ React / Vue はダイアログの `className` に対応します。Vue は文字列・配列・オブジェクト形式の `class` も受け取り、`className` と結合します。再作成ボタンは親が渡した最新の `onRefresh` を呼び出します。
230
385
 
231
386
  | エントリー | 内容 |
232
387
  | --- | --- |
@@ -239,7 +394,7 @@ import type { Ticket, Result } from '@auth-link/web';
239
394
 
240
395
  - 数字はアプリとの目視照合用です。数字だけで認証や承認はできません。
241
396
  - ブラウザーに生成 seed、端末秘密鍵、テナント秘密情報を渡しません。
242
- - リクエストは最大 120 秒。コード更新で期限は延長されません。サービスとアプリはモバイルプロトコル 4 に対応する必要があります。
397
+ - リクエストは最大 120 秒。コード更新で期限は延長されません。サービスとアプリはモバイルプロトコル 7 に対応する必要があります。
243
398
  - サーバー時刻と単調増加時計で残り時間を計算し、通信が止まっても期限切れの数字は隠します。更新境界で短い待ち時間が生じる場合があります。
244
399
  - アンマウントや ticket 変更で監視を中止します。インスタンス内のイベント重複防止とは別に、バックエンドで永続的な冪等処理が必要です。
245
400
  - フロントエンドのイベントは UI 更新用です。保護 API はバックエンドの信頼できる記録で判断します。
@@ -247,4 +402,4 @@ import type { Ticket, Result } from '@auth-link/web';
247
402
 
248
403
  ## ライセンス
249
404
 
250
- UNLICENSED。公開パッケージであってもオープンソース利用許諾は付与されません。同梱ライブラリの帰属表示は `THIRD_PARTY_NOTICES.md` を参照してください。
405
+ MIT。ライセンス全文は `LICENSE`、同梱ライブラリの帰属表示は `THIRD_PARTY_NOTICES.md` を参照してください。