@auth-link/web 0.0.5 → 2.0.0

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/README.md CHANGED
@@ -4,847 +4,199 @@
4
4
 
5
5
  ## English
6
6
 
7
- Frontend dialogs for phone binding and in-app confirmation, with Vue 3, React 18/19 and plain JavaScript. Follow installation, session loading, client initialization, dialogs and event cleanup in this order.
7
+ ### Upgrading from 1.x
8
8
 
9
- ### 1. Install and distinguish the two API origins
9
+ All dialogs now use the minimal layout. Remove the variant option/prop, modal.setVariant() calls and DialogVariant type imports. Language switching and verification callbacks are unchanged.
10
10
 
11
- ```sh
12
- npm install @auth-link/web@latest
13
- # Also install the framework your project uses: vue, or react / react-dom.
14
- ```
15
-
16
- | Origin | Frontend option | Purpose |
17
- | --- | --- | --- |
18
- | Your application API, e.g. `https://cms-api.example.com` | baseUrl, allowedBackendOrigins | Session, request creation and results |
19
- | AuthLink production API `https://api.getauthlink.com` | authLinkOrigin | Validate the AuthLink origin in the ticket |
20
-
21
- baseUrl is your own application API, not the AuthLink API or marketing site. The SDK requires your existing website login/session and the request routes below; it does not provide login/session endpoints. Never put tenant keys in browser code.
22
-
23
- The root import provides clients and types; /vue and /react provide components and useAuthLink; explicitly import /styles.css. ESM and TypeScript, Node 22.13+ tooling; React 18.2+/19 or Vue 3.3+. Install only your framework.
11
+ ### Direct rotating-code verification
24
12
 
25
- ### 2. Load the current session and initialize a client
13
+ The app directly displays a six-digit rotating code. No per-action approval, authorization requestId, businessId or polling is required. First-time enrollment still scans a QR and verifies the phone; entering its current code on the website completes binding.
26
14
 
27
- ```ts
28
- import { createAuthLinkClient } from '@auth-link/web';
29
- import '@auth-link/web/styles.css';
15
+ The CMS backend must reserve one immutable pending action per session before opening the prompt. Another tab cannot replace it. Obtain the user from the server session and check CSRF and business permissions. Verify the passcode on the server and atomically commit verificationId with the saved operation before returning success. onVerified only updates the UI.
30
16
 
31
- // getCsrf reads your website's CURRENT session token on every request.
32
- export function createWebsiteClient(getCsrf: () => string, backendOrigin?: string) {
33
- return createAuthLinkClient({
34
- baseUrl: backendOrigin ? `${backendOrigin}/api/authlink` : '/api/authlink',
35
- allowedBackendOrigins: backendOrigin ? [backendOrigin] : [],
36
- credentials: backendOrigin ? 'include' : 'same-origin',
37
- csrfToken: getCsrf,
38
- authLinkOrigin: 'https://api.getauthlink.com',
39
- });
40
- }
17
+ ```sh
18
+ npm install @auth-link/web@^2.0.0
41
19
  ```
42
20
 
43
- After website login, read your existing session endpoint. This example uses the CMS response authenticated/csrf. If your response field is csrfToken, read that field instead and supply it to the SDK csrfToken callback.
44
-
45
21
  ```ts
46
- // Run in your website's existing authenticated browser flow.
47
- const backendOrigin = 'https://cms-api.example.com'; // YOUR application backend
48
- const response = await fetch(`${backendOrigin}/api/session`, {
49
- credentials: 'include', cache: 'no-store', redirect: 'error',
22
+ import { openPasscode, createPasscodeClient } from '@auth-link/web';
23
+ import '@auth-link/web/styles.css';
24
+ const client = createPasscodeClient({ baseUrl: '/api', csrfToken: () => csrfToken });
25
+ const modal = openPasscode({
26
+ client, expiresAt: pending.expiresAt, locale: 'zh-CN',
27
+ // First-time enrollment only:
28
+ // bindingQr: pending.bindingQr, authLinkOrigin: 'https://api.getauthlink.com',
29
+ onVerified: result => updateUI(result.verificationId),
50
30
  });
51
- if (!response.ok) throw new Error('Session unavailable');
52
- let session = await response.json();
53
- if (session.authenticated !== true || typeof session.csrf !== 'string' || !session.csrf) {
54
- throw new Error('Log in to your website first');
55
- }
56
- const client = createWebsiteClient(() => session.csrf, backendOrigin);
57
- // Same origin: createWebsiteClient(() => session.csrf)
58
- // After token refresh, replace session or update session.csrf.
59
- // On logout/account/session change, close/unmount old dialogs and recreate client state.
31
+ // modal.setLocale('en'); modal.destroy();
60
32
  ```
61
33
 
62
- GET /api/session is your application's example route; adapt it. Same-origin uses relative URLs and same-origin cookies. Cross-origin cookies need include and an application API that already allows your frontend origin; browser cookie policy still applies. Do not read HttpOnly cookies, generate your own CSRF token, or substitute a matching number.
34
+ The default client posts only {passcode} to POST /api/auth with current CMS credentials and CSRF. To submit a form at the same time, supply your own client.verify callback and send {form,passcode} to your CMS. The CMS checks the form against its saved snapshot; never send tenant keys or passcode seeds to the browser.
63
35
 
64
- For existing Bearer/JWT login, auth below is your website's mutable login state. Read its refreshed accessToken on every request. This is not an AuthLink tenant key. CSRF may be omitted only for Bearer with credentials=omit; sending cookies still requires csrfToken.
36
+ ### Options and callbacks
65
37
 
66
- ```ts
67
- const backendOrigin = 'https://cms-api.example.com'; // YOUR application backend
68
- const client = createAuthLinkClient({
69
- baseUrl: `${backendOrigin}/api/authlink`, allowedBackendOrigins: [backendOrigin],
70
- credentials: 'omit', accessToken: () => auth.accessToken,
71
- authLinkOrigin: 'https://api.getauthlink.com',
72
- });
73
- ```
38
+ Required: client.verify(passcode, signal) and expiresAt (the pending CMS action deadline). Optional: locale, bindingQr and authLinkOrigin for first enrollment only, onVerified, onError, onClose, onExpired. Success contains status, verificationId, authenticationPolicy and expiresAt; it has no requestId or businessId.
74
39
 
75
- ### 3. Vue: create on click, display and clean up
40
+ Cookie authentication requires CSRF. Bearer authentication uses accessToken with credentials=omit. For a cross-origin CMS explicitly set allowedBackendOrigins. Bare HTTPS origins are required outside loopback development.
76
41
 
77
- The package includes `examples/vue-client.vue`; copy it as AuthLinkPanel.vue. After login pass the current session.csrf, and backendOrigin for cross-origin calls. Unmount on logout/account/session changes and mount a new instance. Update the csrf prop after token refresh.
42
+ ### React and Vue
78
43
 
79
- ```vue
80
- <script setup lang="ts">
81
- import { markRaw, onUnmounted, ref, shallowRef } from 'vue';
82
- import { createAuthLinkClient, requestKey } from '@auth-link/web';
83
- import type { Confirmation, Mode, Ticket } from '@auth-link/web';
84
- import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/vue';
85
- import '@auth-link/web/styles.css';
44
+ The React component takes the same options. Vue emits verified, error, close and expired. Keep client references stable. Thirteen languages remain supported; Simplified Chinese is the default. All dialogs use one responsive minimal layout.
86
45
 
87
- // csrf comes from your website's current authenticated session.
88
- const props = defineProps<{ csrf: string; backendOrigin?: string }>();
89
- const ticket = shallowRef<Ticket | null>(null);
90
- const busy = ref(false), message = ref('');
91
- const lifetime = new AbortController();
92
- let creationKey: string | null = null;
93
- let creationMode: Mode | null = null;
94
- const client = markRaw(createAuthLinkClient({
95
- baseUrl: props.backendOrigin ? `${props.backendOrigin}/api/authlink` : '/api/authlink',
96
- allowedBackendOrigins: props.backendOrigin ? [props.backendOrigin] : [],
97
- credentials: props.backendOrigin ? 'include' : 'same-origin',
98
- csrfToken: () => props.csrf,
99
- authLinkOrigin: 'https://api.getauthlink.com',
100
- }));
101
- function finish(text: string) { ticket.value = null; message.value = text; }
102
- function confirmed(result: Confirmation) {
103
- finish(`${result.purpose}: ${result.requestId}`);
104
- // Reload your website's account/status API if the page needs fresh state.
105
- }
106
- function showError(error: Error) { message.value = error.message; }
107
- async function start(mode: Mode) {
108
- if (busy.value || ticket.value) return;
109
- busy.value = true; message.value = '';
110
- try {
111
- if (creationMode !== mode) { creationKey = null; creationMode = mode; }
112
- creationKey ??= requestKey();
113
- const value = await client.create(mode, { idempotencyKey: creationKey, signal: lifetime.signal });
114
- if (lifetime.signal.aborted) return;
115
- ticket.value = value; creationKey = null;
116
- } catch (error) {
117
- if (!lifetime.signal.aborted) showError(error as Error);
118
- // Keep the same key when retrying an uncertain creation of the same mode.
119
- } finally { busy.value = false; }
120
- }
121
- onUnmounted(() => { lifetime.abort(); ticket.value = null; creationKey = null; });
46
+ ```tsx
47
+ import { AuthLinkPasscode } from '@auth-link/web/react';
48
+ // Keep client stable (for example with useMemo).
49
+ <AuthLinkPasscode client={client} expiresAt={pending.expiresAt}
50
+ locale="zh-CN" onVerified={updateUI} />
51
+ ```
52
+ ```vue
53
+ <script setup>
54
+ import { AuthLinkPasscode } from '@auth-link/web/vue';
122
55
  </script>
123
-
124
56
  <template>
125
- <button :disabled="busy || !!ticket" @click="start('bind')">Bind phone</button>
126
- <button :disabled="busy || !!ticket" @click="start('confirm')">Confirm</button>
127
- <AuthLinkBind v-if="ticket?.purpose === 'bind'" :ticket="ticket" :client="client" variant="B"
128
- @confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected')"
129
- @expired="finish('Expired; start again')" @error="showError" />
130
- <AuthLinkConfirm v-else-if="ticket?.purpose === 'confirm'" :ticket="ticket" :client="client" variant="B"
131
- @confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected')"
132
- @expired="finish('Expired; start again')" @error="showError" />
133
- <p role="status">{{ message }}</p>
57
+ <AuthLinkPasscode :client="client" :expires-at="pending.expiresAt"
58
+ locale="zh-CN" @verified="updateUI" />
134
59
  </template>
135
60
  ```
136
61
 
137
- Keep ticket in shallowRef and client in markRaw. Retain a failed creation's key when retrying the same mode; clear it only after success. Create only in user handlers, never mounted/watch. Confirmation, rejection, expiry and close clear the ticket and unmount the dialog. Keep the dialog on network errors and use its retry control for the current request. Reload actual account state from your own API when needed.
138
-
139
- ### 4. React
140
-
141
- Pass a stable client created as above and mode=bind or confirm. Unmount the instance on logout/account/session changes.
142
-
143
- ```tsx
144
- "use client";
145
- import { useEffect, useRef, useState } from 'react';
146
- import { requestKey } from '@auth-link/web';
147
- import type { AuthLinkClient, Mode, Ticket } from '@auth-link/web';
148
- import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/react';
149
- // Import '@auth-link/web/styles.css' once in your application entry.
150
-
151
- export function AuthLinkPanel({ client, mode }: { client: AuthLinkClient; mode: Mode }) {
152
- const [ticket, setTicket] = useState<Ticket | null>(null);
153
- const [busy, setBusy] = useState(false);
154
- const [message, setMessage] = useState('');
155
- const creation = useRef<{ mode: Mode; key: string } | null>(null);
156
- const inFlight = useRef(false);
157
- const controller = useRef<AbortController | null>(null);
158
- useEffect(() => () => { controller.current?.abort(); }, []);
159
- function finish(text: string) { setTicket(null); setMessage(text); }
160
- async function start() {
161
- if (inFlight.current || ticket) return;
162
- inFlight.current = true; setBusy(true); setMessage('');
163
- const attempt = new AbortController(); controller.current = attempt;
164
- try {
165
- if (creation.current?.mode !== mode) creation.current = { mode, key: requestKey() };
166
- const value = await client.create(mode, { idempotencyKey: creation.current.key, signal: attempt.signal });
167
- if (attempt.signal.aborted) return;
168
- setTicket(value); creation.current = null;
169
- } catch (error) { if (!attempt.signal.aborted) setMessage((error as Error).message); }
170
- finally {
171
- inFlight.current = false;
172
- if (!attempt.signal.aborted) setBusy(false);
173
- }
174
- }
175
- const Component = mode === 'bind' ? AuthLinkBind : AuthLinkConfirm;
176
- return <>
177
- {!ticket && <button disabled={busy} onClick={() => void start()}>Start verification</button>}
178
- {ticket && <Component ticket={ticket} client={client} variant="B"
179
- onConfirmed={result => finish(`${result.purpose}: ${result.requestId}`)}
180
- onClose={() => finish('Closed')} onRejected={() => finish('Rejected')}
181
- onExpired={() => finish('Expired; start again')}
182
- onError={error => setMessage(error.message)} />}
183
- <p role="status">{message}</p>
184
- </>;
185
- }
186
- ```
187
-
188
- ### 5. Plain JavaScript / direct dialogs
189
-
190
- ```ts
191
- import { openAuthLinkConfirm, requestKey } from '@auth-link/web';
192
- import '@auth-link/web/styles.css';
193
-
194
- // Inside a click handler; client is the stable instance created above.
195
- const ticket = await client.create('confirm', { idempotencyKey: requestKey() });
196
- const modal = openAuthLinkConfirm({
197
- ticket, client, variant: 'B', locale: 'zh-CN',
198
- onConfirmed: result => console.log(result.requestId),
199
- onRejected: result => console.log(result.code),
200
- onExpired: () => console.log('Expired'),
201
- onError: error => console.log(error.message),
202
- onClose: () => console.log('Closed'),
203
- });
204
- // Route/component cleanup: modal.destroy();
205
- // Optional: modal.close(); modal.retry(); modal.setLocale('ja'); modal.setVariant('C');
206
- ```
207
-
208
- For binding use client.create('bind', ...) and openAuthLinkBind(...). Confirmation/rejection dismiss before their callbacks and do not emit onClose. Close stops frontend observation; call destroy when leaving the page/component. Success is delivered once per request per instance. Callbacks update UI; protected actions still call your application's API.
209
-
210
- ### 6. API shapes the frontend needs
211
-
212
- | Application route | Frontend sends | Frontend reads |
213
- | --- | --- | --- |
214
- | POST /api/authlink/requests | purpose=bind or confirm; Idempotency-Key | Ticket |
215
- | POST /api/authlink/requests/{requestId}/result | empty object; current login credentials | Result; polled automatically |
216
-
217
- Both send the current CSRF header in cookie mode. Ticket contains requestId, purpose, qr, pairingCode and expiresAt; confirm also requires displayCode. Import Ticket/Result/Confirmation rather than inventing QR data or matching numbers. Only status=confirmed triggers success. new/pending/approved/consumed are waiting states; rejected/expired trigger their events.
218
-
219
- SDK create() sends purpose only. If your application expects additional fields, create via your own fetch, validate the returned ticket and pass it to the same component. post, session and backendOrigin below are your frontend state; retain the creation key across retries of the same payload.
220
-
221
- ```ts
222
- import { requestKey, validateTicket } from '@auth-link/web';
223
-
224
- // Keep this key across retries of the SAME operation and payload.
225
- const idempotencyKey = requestKey();
226
- const response = await fetch(`${backendOrigin}/api/authlink/requests`, {
227
- method: 'POST', credentials: 'include', cache: 'no-store', redirect: 'error',
228
- headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': session.csrf,
229
- 'Idempotency-Key': idempotencyKey },
230
- body: JSON.stringify({ purpose: 'confirm', postId: post.id, version: post.version }),
231
- });
232
- if (!response.ok) throw new Error('Creation failed');
233
- const ticket = validateTicket(await response.json(), 'confirm', 'https://api.getauthlink.com');
234
- // Pass ticket and the same client to AuthLinkConfirm.
235
- ```
236
-
237
- ### 7. Props, events and presentation
238
-
239
- | Prop | Type | Meaning |
240
- | --- | --- | --- |
241
- | `ticket` | `Ticket` | Required; the current request |
242
- | `client` | `AuthLinkClient` | Required; keep the instance stable |
243
- | `locale` | `Locale` | Default `zh-CN` |
244
- | `variant` | `'A' / 'B' / 'C'` | Default `A` |
245
- | `showQr` | `boolean` | Optional QR for confirm; bind always shows QR |
246
- | `intervalMs` | `number` | Default 1500; range 250–10000 ms |
247
- | `onRefresh` | `() => void` | Optional user action after expiry; create a new ticket |
248
- | `className` | `string` | Custom dialog class; Vue also accepts `class` |
249
-
250
- | React callback / Vue event | Value | Behavior |
251
- | --- | --- | --- |
252
- | `onConfirmed` / `confirmed` | `Confirmation` | Success; dialog closes automatically |
253
- | `onRejected` / `rejected` | `Rejection` | `{status:'rejected',code:'REJECTED',requestId,purpose}`; dialog closes |
254
- | `onExpired` / `expired` | — | Request expired |
255
- | `onClose` / `close` | — | User closes the dialog; clear ticket |
256
- | `onError` / `error` | `Error` | Read `error.message`; retain the dialog for retry |
257
-
258
- A is a centered card (default), B splits on desktop and stacks on narrow screens, and C is compact with a mobile bottom sheet. Changing locale/variant does not create a request. Browser support for native dialog.showModal() is required.
259
-
260
- Import CSS explicitly. Dialogs mount under document.body; set --authlink-foreground, --authlink-background, --authlink-accent and --authlink-muted on :root or .authlink-dialog. Vue class accepts strings/arrays/objects alongside className. SSR renders no dialog and creates no requests.
261
-
262
- Locales: `zh-CN` (default), `zh-TW`, `en`, `ja`, `bn`, `es`, `fil`, `hi`, `id`, `ms`, `pt`, `th`, `vi`. Missing/unsupported values fall back to Simplified Chinese. Import supportedLocales for the list.
263
-
264
- ### 8. Transport options and troubleshooting
62
+ ### Errors and cleanup
265
63
 
266
- | Option | Purpose |
267
- | --- | --- |
268
- | `baseUrl` | Your application API prefix; default `/api/authlink` |
269
- | `allowedBackendOrigins` | Exact extra application API origins; no paths/wildcards |
270
- | `credentials` | Default `same-origin`; cross-origin cookies need `include` |
271
- | `csrfToken` | Sync/async getter for the current website CSRF token |
272
- | `csrfHeaderName` | Default `X-CSRF-Token` |
273
- | `accessToken` | Sync/async website Bearer token getter, without `Bearer ` |
274
- | `headers` | Extra headers or sync/async getter |
275
- | `authLinkOrigin` | `https://api.getauthlink.com` for production AuthLink |
276
- | `fetch` | Optional fetch implementation |
64
+ A used code cannot authorize a second action, even in another session. After sign-in or first enrollment, wait for the next code. PASSCODE_INVALID/PASSCODE_USED/PASSCODE_LOCKED are validation errors, not success. Retain the same backend idempotency key and original code when a result is unknown; retries must not repeat the business action.
277
65
 
278
- Custom headers cannot replace Accept, Content-Type, Authorization, Idempotency-Key or the configured CSRF header. Read error.message: CSRF_REQUIRED means check the session token; ACCESS_TOKEN_REQUIRED means check login state; BACKEND_ORIGIN_NOT_ALLOWED means check exact allowed origins; NETWORK_ERROR means inspect browser Network, connectivity and CORS. Requests time out after 10 seconds; closing/unmounting cancels observation.
66
+ destroy() aborts browser work and clears timers without a close callback. close() also calls onClose. Closing a dialog does not roll back server work: your CMS must manage cancellation and its pending slot. Waiting and local expiry never poll.
279
67
 
280
- Expired digits are hidden; do not generate or cache new ones in the frontend. QR generation is local; allow data: images if your page uses CSP. A callback, localStorage or a browser-supplied status is not authorization.
281
-
282
- ### License
283
-
284
- MIT. See LICENSE and THIRD_PARTY_NOTICES.md.
68
+ The older openAuthLink / Ticket / Consume workflow remains available for the earlier protocol; new apps use openPasscode / AuthLinkPasscode and the backend Authenticate/authenticate method. No old-binding migration is provided.
285
69
 
286
70
  ---
287
71
 
288
72
  ## 简体中文
289
73
 
290
- 用于手机绑定与 App 确认的前端组件,支持 Vue 3、React 18/19 和原生 JavaScript。本文按前端接入顺序说明安装、登录状态读取、客户端初始化、弹窗和事件处理。
291
-
292
- ### 1. 安装并区分两个 API 地址
74
+ ### 从 1.x 升级
293
75
 
294
- ```sh
295
- npm install @auth-link/web@latest
296
- # 另行安装你的项目使用的 vue,或 react / react-dom。
297
- ```
76
+ 所有弹窗统一使用极简布局。请删除 variant 参数或组件属性、modal.setVariant() 调用和 DialogVariant 类型导入。语言切换与验证回调保持不变。
298
77
 
299
- | 地址 | 前端配置 | 用途 |
300
- | --- | --- | --- |
301
- | 你的网站业务后台,如 `https://cms-api.example.com` | `baseUrl`、`allowedBackendOrigins` | 登录状态、创建请求和查询结果 |
302
- | AuthLink 生产 API:`https://api.getauthlink.com` | `authLinkOrigin` | 核对 ticket 中的 AuthLink 服务来源 |
78
+ ### 直接验证动态码
303
79
 
304
- `baseUrl` 不是 AuthLink API,也不是 AuthLink 官网。接入方的网站和业务 API 仍使用自己的域名。需要已有的网站登录状态和下文的请求接口;Web SDK 不提供登录或会话接口。租户密钥不能放到浏览器。
80
+ App 直接显示六位动态码。日常验证不需要逐次批准、授权 requestId、businessId 或轮询。首次绑定仍需扫码验证设备,在网站输入 App 当前动态码后才完成绑定。
305
81
 
306
- 导入入口:根入口提供客户端和类型;`/vue`、`/react` 提供组件及 useAuthLink;`/styles.css` 必须显式导入。ESM、TypeScript,工具环境 Node 22.13+;React 18.2+/19、Vue 3.3+,只安装所用框架。
307
-
308
- ### 2. 读取当前登录状态并创建客户端
309
-
310
- ```ts
311
- import { createAuthLinkClient } from '@auth-link/web';
312
- import '@auth-link/web/styles.css';
82
+ 打开弹窗前,CMS 后台必须为会话原子保留唯一且不可变的待办。其他标签页不能覆盖它。用户来自服务端会话,后台检查 CSRF、业务权限和原表单,验证动态码后将 verificationId 与原操作原子提交,再返回成功。onVerified 只更新界面。
313
83
 
314
- // getCsrf reads your website's CURRENT session token on every request.
315
- export function createWebsiteClient(getCsrf: () => string, backendOrigin?: string) {
316
- return createAuthLinkClient({
317
- baseUrl: backendOrigin ? `${backendOrigin}/api/authlink` : '/api/authlink',
318
- allowedBackendOrigins: backendOrigin ? [backendOrigin] : [],
319
- credentials: backendOrigin ? 'include' : 'same-origin',
320
- csrfToken: getCsrf,
321
- authLinkOrigin: 'https://api.getauthlink.com',
322
- });
323
- }
84
+ ```sh
85
+ npm install @auth-link/web@^2.0.0
324
86
  ```
325
87
 
326
- 先完成你的网站登录,再读取应用已有的会话接口。下面使用与 CMS 一致的 `{authenticated, csrf}` 响应;如果你的后台字段名为 csrfToken,读取它再交给 SDK 的 csrfToken 回调即可。
327
-
328
88
  ```ts
329
- // Run in your website's existing authenticated browser flow.
330
- const backendOrigin = 'https://cms-api.example.com'; // YOUR application backend
331
- const response = await fetch(`${backendOrigin}/api/session`, {
332
- credentials: 'include', cache: 'no-store', redirect: 'error',
89
+ import { openPasscode, createPasscodeClient } from '@auth-link/web';
90
+ import '@auth-link/web/styles.css';
91
+ const client = createPasscodeClient({ baseUrl: '/api', csrfToken: () => csrfToken });
92
+ const modal = openPasscode({
93
+ client, expiresAt: pending.expiresAt, locale: 'zh-CN',
94
+ // First-time enrollment only:
95
+ // bindingQr: pending.bindingQr, authLinkOrigin: 'https://api.getauthlink.com',
96
+ onVerified: result => updateUI(result.verificationId),
333
97
  });
334
- if (!response.ok) throw new Error('Session unavailable');
335
- let session = await response.json();
336
- if (session.authenticated !== true || typeof session.csrf !== 'string' || !session.csrf) {
337
- throw new Error('Log in to your website first');
338
- }
339
- const client = createWebsiteClient(() => session.csrf, backendOrigin);
340
- // Same origin: createWebsiteClient(() => session.csrf)
341
- // After token refresh, replace session or update session.csrf.
342
- // On logout/account/session change, close/unmount old dialogs and recreate client state.
98
+ // modal.setLocale('en'); modal.destroy();
343
99
  ```
344
100
 
345
- `GET /api/session` 是网站已有接口的示例,请按实际路径替换。同源可以使用相对路径和 same-origin Cookie;跨域 Cookie 使用 include,并要求当前前端来源已被业务后台允许。Cookie 策略仍可能限制请求。浏览器无法读取 HttpOnly Cookie,只需要传回当前会话的 CSRF token;不能用手机核对码或浏览器随机值代替。
101
+ 默认客户端向 CMS 的 POST /api/auth 提交 {passcode},携带当前会话与 CSRF。要随表单提交,可自行实现 client.verify,向 CMS 发送 {form,passcode};后台核对原保存快照。租户密钥与动态码密钥不能进入浏览器。
346
102
 
347
- 已有 Bearer / JWT 登录的项目可用下列配置,auth 是你的网站登录状态,刷新流程更新 auth.accessToken。此 token 不是 AuthLink 租户密钥。仅在 credentials=omit 的 Bearer 模式可以不传 CSRF;若同时发送 Cookie,仍需 csrfToken。
103
+ ### 选项与回调
348
104
 
349
- ```ts
350
- const backendOrigin = 'https://cms-api.example.com'; // YOUR application backend
351
- const client = createAuthLinkClient({
352
- baseUrl: `${backendOrigin}/api/authlink`, allowedBackendOrigins: [backendOrigin],
353
- credentials: 'omit', accessToken: () => auth.accessToken,
354
- authLinkOrigin: 'https://api.getauthlink.com',
355
- });
356
- ```
105
+ 必填:client.verify(passcode, signal)、expiresAt(CMS 待办截止时间)。可选:locale、仅首次绑定使用的 bindingQr 与 authLinkOrigin、onVerified、onError、onClose、onExpired。成功结果含 status、verificationId、authenticationPolicy、expiresAt,不含 requestId 或 businessId。
357
106
 
358
- ### 3. Vue:点击创建、展示和清理
107
+ Cookie 模式必须配置 CSRF。Bearer 模式配置 accessToken 且 credentials=omit。跨域 CMS 必须显式配置 allowedBackendOrigins;回环开发之外使用 HTTPS。
359
108
 
360
- 完整文件随包提供为 `examples/vue-client.vue`,可复制为 AuthLinkPanel.vue。父组件在登录后传入当前 session.csrf;跨域时额外传 backendOrigin。退出或切换账号/session 时卸载旧组件,重新挂载新实例;CSRF 刷新可更新 csrf prop。
109
+ ### React 与 Vue
361
110
 
362
- ```vue
363
- <script setup lang="ts">
364
- import { markRaw, onUnmounted, ref, shallowRef } from 'vue';
365
- import { createAuthLinkClient, requestKey } from '@auth-link/web';
366
- import type { Confirmation, Mode, Ticket } from '@auth-link/web';
367
- import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/vue';
368
- import '@auth-link/web/styles.css';
111
+ React 组件使用相同选项,Vue 发出 verified、error、close、expired 事件。client 引用需保持稳定。支持 13 种语言,默认简体中文;统一使用响应式极简布局。
369
112
 
370
- // csrf comes from your website's current authenticated session.
371
- const props = defineProps<{ csrf: string; backendOrigin?: string }>();
372
- const ticket = shallowRef<Ticket | null>(null);
373
- const busy = ref(false), message = ref('');
374
- const lifetime = new AbortController();
375
- let creationKey: string | null = null;
376
- let creationMode: Mode | null = null;
377
- const client = markRaw(createAuthLinkClient({
378
- baseUrl: props.backendOrigin ? `${props.backendOrigin}/api/authlink` : '/api/authlink',
379
- allowedBackendOrigins: props.backendOrigin ? [props.backendOrigin] : [],
380
- credentials: props.backendOrigin ? 'include' : 'same-origin',
381
- csrfToken: () => props.csrf,
382
- authLinkOrigin: 'https://api.getauthlink.com',
383
- }));
384
- function finish(text: string) { ticket.value = null; message.value = text; }
385
- function confirmed(result: Confirmation) {
386
- finish(`${result.purpose}: ${result.requestId}`);
387
- // Reload your website's account/status API if the page needs fresh state.
388
- }
389
- function showError(error: Error) { message.value = error.message; }
390
- async function start(mode: Mode) {
391
- if (busy.value || ticket.value) return;
392
- busy.value = true; message.value = '';
393
- try {
394
- if (creationMode !== mode) { creationKey = null; creationMode = mode; }
395
- creationKey ??= requestKey();
396
- const value = await client.create(mode, { idempotencyKey: creationKey, signal: lifetime.signal });
397
- if (lifetime.signal.aborted) return;
398
- ticket.value = value; creationKey = null;
399
- } catch (error) {
400
- if (!lifetime.signal.aborted) showError(error as Error);
401
- // Keep the same key when retrying an uncertain creation of the same mode.
402
- } finally { busy.value = false; }
403
- }
404
- onUnmounted(() => { lifetime.abort(); ticket.value = null; creationKey = null; });
113
+ ```tsx
114
+ import { AuthLinkPasscode } from '@auth-link/web/react';
115
+ // Keep client stable (for example with useMemo).
116
+ <AuthLinkPasscode client={client} expiresAt={pending.expiresAt}
117
+ locale="zh-CN" onVerified={updateUI} />
118
+ ```
119
+ ```vue
120
+ <script setup>
121
+ import { AuthLinkPasscode } from '@auth-link/web/vue';
405
122
  </script>
406
-
407
123
  <template>
408
- <button :disabled="busy || !!ticket" @click="start('bind')">Bind phone</button>
409
- <button :disabled="busy || !!ticket" @click="start('confirm')">Confirm</button>
410
- <AuthLinkBind v-if="ticket?.purpose === 'bind'" :ticket="ticket" :client="client" variant="B"
411
- @confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected')"
412
- @expired="finish('Expired; start again')" @error="showError" />
413
- <AuthLinkConfirm v-else-if="ticket?.purpose === 'confirm'" :ticket="ticket" :client="client" variant="B"
414
- @confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected')"
415
- @expired="finish('Expired; start again')" @error="showError" />
416
- <p role="status">{{ message }}</p>
124
+ <AuthLinkPasscode :client="client" :expires-at="pending.expiresAt"
125
+ locale="zh-CN" @verified="updateUI" />
417
126
  </template>
418
127
  ```
419
128
 
420
- 示例使用 shallowRef 保存 ticket、markRaw 保存 client。创建失败保留同一用途的原幂等键;成功后才清空。创建只发生在点击事件中,mounted/watch 不创建请求。确认、拒绝、过期或用户关闭时清空 ticket,卸载组件;网络错误保留弹窗,使用弹窗的重试按钮继续当前请求。需要真实账号状态时重新查询你的网站状态接口。
129
+ ### 失败与清理
421
130
 
422
- ### 4. React
131
+ 已使用的码不能用于另一个操作或会话;刚登录或首次绑定后需等待下一组数字。PASSCODE_INVALID / PASSCODE_USED / PASSCODE_LOCKED 是验证失败,不能当成成功。网络结果不明时,后台保留原幂等键和原码重试,业务不能重复执行。
423
132
 
424
- 使用上面的初始化方式创建稳定 client,再传给此组件;mode 为 bind 或 confirm。退出或切换账号/session 时卸载旧实例。
133
+ destroy() 中止浏览器请求并清理计时器,不触发关闭回调;close() 还调用 onClose。关闭弹窗不会回滚服务端操作,取消与会话待办由 CMS 管理。等待和本地过期均不轮询。
425
134
 
426
- ```tsx
427
- "use client";
428
- import { useEffect, useRef, useState } from 'react';
429
- import { requestKey } from '@auth-link/web';
430
- import type { AuthLinkClient, Mode, Ticket } from '@auth-link/web';
431
- import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/react';
432
- // Import '@auth-link/web/styles.css' once in your application entry.
433
-
434
- export function AuthLinkPanel({ client, mode }: { client: AuthLinkClient; mode: Mode }) {
435
- const [ticket, setTicket] = useState<Ticket | null>(null);
436
- const [busy, setBusy] = useState(false);
437
- const [message, setMessage] = useState('');
438
- const creation = useRef<{ mode: Mode; key: string } | null>(null);
439
- const inFlight = useRef(false);
440
- const controller = useRef<AbortController | null>(null);
441
- useEffect(() => () => { controller.current?.abort(); }, []);
442
- function finish(text: string) { setTicket(null); setMessage(text); }
443
- async function start() {
444
- if (inFlight.current || ticket) return;
445
- inFlight.current = true; setBusy(true); setMessage('');
446
- const attempt = new AbortController(); controller.current = attempt;
447
- try {
448
- if (creation.current?.mode !== mode) creation.current = { mode, key: requestKey() };
449
- const value = await client.create(mode, { idempotencyKey: creation.current.key, signal: attempt.signal });
450
- if (attempt.signal.aborted) return;
451
- setTicket(value); creation.current = null;
452
- } catch (error) { if (!attempt.signal.aborted) setMessage((error as Error).message); }
453
- finally {
454
- inFlight.current = false;
455
- if (!attempt.signal.aborted) setBusy(false);
456
- }
457
- }
458
- const Component = mode === 'bind' ? AuthLinkBind : AuthLinkConfirm;
459
- return <>
460
- {!ticket && <button disabled={busy} onClick={() => void start()}>Start verification</button>}
461
- {ticket && <Component ticket={ticket} client={client} variant="B"
462
- onConfirmed={result => finish(`${result.purpose}: ${result.requestId}`)}
463
- onClose={() => finish('Closed')} onRejected={() => finish('Rejected')}
464
- onExpired={() => finish('Expired; start again')}
465
- onError={error => setMessage(error.message)} />}
466
- <p role="status">{message}</p>
467
- </>;
468
- }
469
- ```
470
-
471
- ### 5. 原生 JavaScript / 直接调用
472
-
473
- ```ts
474
- import { openAuthLinkConfirm, requestKey } from '@auth-link/web';
475
- import '@auth-link/web/styles.css';
476
-
477
- // Inside a click handler; client is the stable instance created above.
478
- const ticket = await client.create('confirm', { idempotencyKey: requestKey() });
479
- const modal = openAuthLinkConfirm({
480
- ticket, client, variant: 'B', locale: 'zh-CN',
481
- onConfirmed: result => console.log(result.requestId),
482
- onRejected: result => console.log(result.code),
483
- onExpired: () => console.log('Expired'),
484
- onError: error => console.log(error.message),
485
- onClose: () => console.log('Closed'),
486
- });
487
- // Route/component cleanup: modal.destroy();
488
- // Optional: modal.close(); modal.retry(); modal.setLocale('ja'); modal.setVariant('C');
489
- ```
490
-
491
- 绑定改用 `client.create('bind', ...)` 和 `openAuthLinkBind(...)`。确认或拒绝会先自动关闭再触发对应回调,不触发 onClose。关闭只停止前端观察;页面或组件退出时必须调用 destroy。每个实例对同一请求只通知一次成功;回调用于界面更新,受保护操作仍请求你的网站业务 API。
492
-
493
- ### 6. 前端需要的接口约定
494
-
495
- | 网站接口 | 前端发送 | 前端读取 |
496
- | --- | --- | --- |
497
- | `POST /api/authlink/requests` | `{purpose:'bind'}` 或 confirm;`Idempotency-Key` | `Ticket` |
498
- | `POST /api/authlink/requests/{requestId}/result` | `{}`;当前登录凭据 | `Result`,由 SDK 自动轮询 |
499
-
500
- Cookie 模式两者均发送当前 CSRF 头。Ticket 包含 requestId、purpose、qr、pairingCode、expiresAt;confirm 还需要 displayCode。直接导入包内 Ticket / Result / Confirmation 类型,不自行生成 QR 内容或动态数字。只有 status=confirmed 触发成功事件;new/pending/approved/consumed 是等待状态,rejected/expired 分别触发对应事件。
501
-
502
- 若你的网站创建接口需要文章 ID、版本等额外参数,SDK create() 只发送 purpose,因此使用自己的 fetch 创建,然后校验 ticket 并交给同一组件。下面的 post、session、backendOrigin 均来自你自己的前端状态;幂等键应保存到重试状态,失败时不要重新生成。
503
-
504
- ```ts
505
- import { requestKey, validateTicket } from '@auth-link/web';
506
-
507
- // Keep this key across retries of the SAME operation and payload.
508
- const idempotencyKey = requestKey();
509
- const response = await fetch(`${backendOrigin}/api/authlink/requests`, {
510
- method: 'POST', credentials: 'include', cache: 'no-store', redirect: 'error',
511
- headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': session.csrf,
512
- 'Idempotency-Key': idempotencyKey },
513
- body: JSON.stringify({ purpose: 'confirm', postId: post.id, version: post.version }),
514
- });
515
- if (!response.ok) throw new Error('Creation failed');
516
- const ticket = validateTicket(await response.json(), 'confirm', 'https://api.getauthlink.com');
517
- // Pass ticket and the same client to AuthLinkConfirm.
518
- ```
519
-
520
- ### 7. 参数、事件与样式
521
-
522
- | 参数 | 类型 | 含义 |
523
- | --- | --- | --- |
524
- | `ticket` | `Ticket` | Required; the current request |
525
- | `client` | `AuthLinkClient` | Required; keep the instance stable |
526
- | `locale` | `Locale` | Default `zh-CN` |
527
- | `variant` | `'A' / 'B' / 'C'` | Default `A` |
528
- | `showQr` | `boolean` | Optional QR for confirm; bind always shows QR |
529
- | `intervalMs` | `number` | Default 1500; range 250–10000 ms |
530
- | `onRefresh` | `() => void` | Optional user action after expiry; create a new ticket |
531
- | `className` | `string` | Custom dialog class; Vue also accepts `class` |
532
-
533
- | React 回调 / Vue 事件 | 参数 | 行为 |
534
- | --- | --- | --- |
535
- | `onConfirmed` / `confirmed` | `Confirmation` | Success; dialog closes automatically |
536
- | `onRejected` / `rejected` | `Rejection` | `{status:'rejected',code:'REJECTED',requestId,purpose}`; dialog closes |
537
- | `onExpired` / `expired` | — | Request expired |
538
- | `onClose` / `close` | — | User closes the dialog; clear ticket |
539
- | `onError` / `error` | `Error` | Read `error.message`; retain the dialog for retry |
540
-
541
- A 为默认居中卡片;B 为桌面分栏、窄屏纵排;C 为紧凑卡片、手机底部弹层。切换 locale/variant 不重新创建请求。原生 dialog.showModal() 浏览器支持是必需的。
542
-
543
- CSS 需单独导入。弹窗挂到 document.body,主题变量放在 :root 或 .authlink-dialog:`--authlink-foreground`、`--authlink-background`、`--authlink-accent`、`--authlink-muted`。Vue 的 class 支持字符串、数组和对象,并与 className 合并。SSR 不展示弹窗或创建请求。
544
-
545
- 语言:`zh-CN` (default), `zh-TW`, `en`, `ja`, `bn`, `es`, `fil`, `hi`, `id`, `ms`, `pt`, `th`, `vi`。未指定或不支持时回退简体中文,可导入 supportedLocales 获取列表。
546
-
547
- ### 8. 传输配置与排错
548
-
549
- | 配置 | 用途 |
550
- | --- | --- |
551
- | `baseUrl` | Your application API prefix; default `/api/authlink` |
552
- | `allowedBackendOrigins` | Exact extra application API origins; no paths/wildcards |
553
- | `credentials` | Default `same-origin`; cross-origin cookies need `include` |
554
- | `csrfToken` | Sync/async getter for the current website CSRF token |
555
- | `csrfHeaderName` | Default `X-CSRF-Token` |
556
- | `accessToken` | Sync/async website Bearer token getter, without `Bearer ` |
557
- | `headers` | Extra headers or sync/async getter |
558
- | `authLinkOrigin` | `https://api.getauthlink.com` for production AuthLink |
559
- | `fetch` | Optional fetch implementation |
560
-
561
- 自定义 headers 不能覆盖 Accept、Content-Type、Authorization、Idempotency-Key 或配置的 CSRF 头。错误信息可从 error.message 读取:CSRF_REQUIRED 检查当前 token;ACCESS_TOKEN_REQUIRED 检查登录 token;BACKEND_ORIGIN_NOT_ALLOWED 检查完整来源允许列表;NETWORK_ERROR 检查浏览器 Network 面板、网络和 CORS。SDK 请求超时为 10 秒,关闭/卸载会取消观察。
562
-
563
- 数字过期会隐藏,不在前端生成或缓存新数字;二维码在浏览器本地生成。若页面启用 CSP,图片来源需要允许 data:。确认回调、localStorage 或浏览器自报状态都不作为业务授权依据。
564
-
565
- ### 许可证
566
-
567
- MIT。完整文本见 LICENSE,第三方归属见 THIRD_PARTY_NOTICES.md。
135
+ 旧 openAuthLink / Ticket / Consume 流程仍供此前协议使用;新 App 使用 openPasscode / AuthLinkPasscode 以及后台 Authenticate / authenticate。此次不提供旧绑定升级。
568
136
 
569
137
  ---
570
138
 
571
139
  ## 日本語
572
140
 
573
- スマートフォン登録とアプリ内確認のフロントエンド用ダイアログです。Vue 3、React 18/19、通常の JavaScript に対応します。インストール、session 取得、client 初期化、表示、イベントと後処理の順に接続します。
574
-
575
- ### 1. インストールと 2 つの API
576
-
577
- ```sh
578
- npm install @auth-link/web@latest
579
- # 利用する vue、または react / react-dom もインストールします。
580
- ```
581
-
582
- | origin | フロントエンド設定 | 用途 |
583
- | --- | --- | --- |
584
- | 自サイトの API(例 https://cms-api.example.com) | baseUrl、allowedBackendOrigins | session、要求作成、結果 |
585
- | AuthLink 本番 API https://api.getauthlink.com | authLinkOrigin | ticket 内のサービス origin の照合 |
141
+ ### 1.x からの更新
586
142
 
587
- baseUrl は自サイトの業務 API であり、AuthLink API や公式サイトではありません。既存のログイン/session と下記要求 API を利用します。SDK 自体はログイン/session API を提供しません。テナントキーはブラウザーに渡しません。
143
+ すべてのダイアログはシンプルレイアウトに統一されました。variant オプション・プロパティ、modal.setVariant() の呼び出し、DialogVariant 型のインポートを削除してください。言語切り替えと認証コールバックは変更ありません。
588
144
 
589
- ルート import は client と型、/vue と /react はコンポーネントと useAuthLink、/styles.css は明示 import です。ESM、TypeScript、ツール環境 Node 22.13+、React 18.2+/19 または Vue 3.3+。利用するフレームワークのみ導入します。
145
+ ### 認証コードを直接検証
590
146
 
591
- ### 2. 現在の session と client
147
+ アプリに6桁の認証コードを直接表示します。通常の検証では、毎回の承認、認証用requestId、businessId、ポーリングは不要です。初回はQRで端末を確認し、サイトで現在のコードを検証して連携を完了します。
592
148
 
593
- ```ts
594
- import { createAuthLinkClient } from '@auth-link/web';
595
- import '@auth-link/web/styles.css';
149
+ 画面を開く前にCMSサーバーでセッションの唯一の保留中操作を原子的に確保します。別タブから上書きさせません。ユーザーはサーバーのセッションから取得し、CSRF、業務権限、元のフォームを確認します。コードを検証し、verificationIdと元の操作を原子的に確定してから成功を返します。onVerifiedは表示更新専用です。
596
150
 
597
- // getCsrf reads your website's CURRENT session token on every request.
598
- export function createWebsiteClient(getCsrf: () => string, backendOrigin?: string) {
599
- return createAuthLinkClient({
600
- baseUrl: backendOrigin ? `${backendOrigin}/api/authlink` : '/api/authlink',
601
- allowedBackendOrigins: backendOrigin ? [backendOrigin] : [],
602
- credentials: backendOrigin ? 'include' : 'same-origin',
603
- csrfToken: getCsrf,
604
- authLinkOrigin: 'https://api.getauthlink.com',
605
- });
606
- }
151
+ ```sh
152
+ npm install @auth-link/web@^2.0.0
607
153
  ```
608
154
 
609
- サイトのログイン後に既存 session API を読みます。例は CMS の authenticated/csrf です。フィールド名が csrfToken ならその値を SDK の csrfToken コールバックへ渡します。
610
-
611
155
  ```ts
612
- // Run in your website's existing authenticated browser flow.
613
- const backendOrigin = 'https://cms-api.example.com'; // YOUR application backend
614
- const response = await fetch(`${backendOrigin}/api/session`, {
615
- credentials: 'include', cache: 'no-store', redirect: 'error',
156
+ import { openPasscode, createPasscodeClient } from '@auth-link/web';
157
+ import '@auth-link/web/styles.css';
158
+ const client = createPasscodeClient({ baseUrl: '/api', csrfToken: () => csrfToken });
159
+ const modal = openPasscode({
160
+ client, expiresAt: pending.expiresAt, locale: 'zh-CN',
161
+ // First-time enrollment only:
162
+ // bindingQr: pending.bindingQr, authLinkOrigin: 'https://api.getauthlink.com',
163
+ onVerified: result => updateUI(result.verificationId),
616
164
  });
617
- if (!response.ok) throw new Error('Session unavailable');
618
- let session = await response.json();
619
- if (session.authenticated !== true || typeof session.csrf !== 'string' || !session.csrf) {
620
- throw new Error('Log in to your website first');
621
- }
622
- const client = createWebsiteClient(() => session.csrf, backendOrigin);
623
- // Same origin: createWebsiteClient(() => session.csrf)
624
- // After token refresh, replace session or update session.csrf.
625
- // On logout/account/session change, close/unmount old dialogs and recreate client state.
165
+ // modal.setLocale('en'); modal.destroy();
626
166
  ```
627
167
 
628
- GET /api/session はサイト側の既存ルートの例です。同 origin は相対 URL と same-origin Cookie、別 origin の Cookie は include を使い、業務 API が現在のフロントエンド origin を許可している必要があります。Cookie 制限も適用されます。HttpOnly Cookie を読んだり、CSRF をブラウザーで生成したり、照合数字で代用したりしません。
168
+ 標準クライアントは現在の認証情報とCSRFを付け、CMSのPOST /api/authへ{passcode}を送信します。フォームも送る場合はclient.verifyで{form,passcode}をCMSへ送信し、サーバーで保存済み内容と照合します。テナントキーやコード生成用秘密はブラウザーへ渡しません。
629
169
 
630
- Bearer/JWT を使う場合、auth は既存ログイン状態です。更新された accessToken を毎回読みます。これはテナントキーではありません。credentials=omit の Bearer のみ CSRF を省略でき、Cookie 併用なら csrfToken が必要です。
170
+ ### オプションとコールバック
631
171
 
632
- ```ts
633
- const backendOrigin = 'https://cms-api.example.com'; // YOUR application backend
634
- const client = createAuthLinkClient({
635
- baseUrl: `${backendOrigin}/api/authlink`, allowedBackendOrigins: [backendOrigin],
636
- credentials: 'omit', accessToken: () => auth.accessToken,
637
- authLinkOrigin: 'https://api.getauthlink.com',
638
- });
639
- ```
172
+ 必須はclient.verify(passcode, signal)とexpiresAt(CMS操作の期限)です。任意でlocale、初回用bindingQrとauthLinkOrigin、onVerified、onError、onClose、onExpiredを指定します。成功結果はstatus、verificationId、authenticationPolicy、expiresAtを含み、requestIdやbusinessIdは含みません。
640
173
 
641
- ### 3. Vue:クリックで作成、表示、後処理
174
+ Cookie認証ではCSRFが必須です。Bearer認証はaccessTokenとcredentials=omitを使います。別オリジンのCMSはallowedBackendOriginsを明示し、ループバック開発以外ではHTTPSを使います。
642
175
 
643
- `examples/vue-client.vue` を AuthLinkPanel.vue としてコピーできます。ログイン後に現在の session.csrf を渡し、別 origin なら backendOrigin も渡します。ログアウト・アカウント/session 変更では旧コンポーネントをアンマウントして再作成します。token 更新時は csrf prop を更新します。
176
+ ### ReactとVue
644
177
 
645
- ```vue
646
- <script setup lang="ts">
647
- import { markRaw, onUnmounted, ref, shallowRef } from 'vue';
648
- import { createAuthLinkClient, requestKey } from '@auth-link/web';
649
- import type { Confirmation, Mode, Ticket } from '@auth-link/web';
650
- import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/vue';
651
- import '@auth-link/web/styles.css';
178
+ Reactは同じオプションを使い、Vueはverified、error、close、expiredイベントを発行します。clientの参照を安定させてください。13言語に対応し、既定は簡体字中国語です。すべてのダイアログはレスポンシブなシンプルレイアウトを使用します。
652
179
 
653
- // csrf comes from your website's current authenticated session.
654
- const props = defineProps<{ csrf: string; backendOrigin?: string }>();
655
- const ticket = shallowRef<Ticket | null>(null);
656
- const busy = ref(false), message = ref('');
657
- const lifetime = new AbortController();
658
- let creationKey: string | null = null;
659
- let creationMode: Mode | null = null;
660
- const client = markRaw(createAuthLinkClient({
661
- baseUrl: props.backendOrigin ? `${props.backendOrigin}/api/authlink` : '/api/authlink',
662
- allowedBackendOrigins: props.backendOrigin ? [props.backendOrigin] : [],
663
- credentials: props.backendOrigin ? 'include' : 'same-origin',
664
- csrfToken: () => props.csrf,
665
- authLinkOrigin: 'https://api.getauthlink.com',
666
- }));
667
- function finish(text: string) { ticket.value = null; message.value = text; }
668
- function confirmed(result: Confirmation) {
669
- finish(`${result.purpose}: ${result.requestId}`);
670
- // Reload your website's account/status API if the page needs fresh state.
671
- }
672
- function showError(error: Error) { message.value = error.message; }
673
- async function start(mode: Mode) {
674
- if (busy.value || ticket.value) return;
675
- busy.value = true; message.value = '';
676
- try {
677
- if (creationMode !== mode) { creationKey = null; creationMode = mode; }
678
- creationKey ??= requestKey();
679
- const value = await client.create(mode, { idempotencyKey: creationKey, signal: lifetime.signal });
680
- if (lifetime.signal.aborted) return;
681
- ticket.value = value; creationKey = null;
682
- } catch (error) {
683
- if (!lifetime.signal.aborted) showError(error as Error);
684
- // Keep the same key when retrying an uncertain creation of the same mode.
685
- } finally { busy.value = false; }
686
- }
687
- onUnmounted(() => { lifetime.abort(); ticket.value = null; creationKey = null; });
180
+ ```tsx
181
+ import { AuthLinkPasscode } from '@auth-link/web/react';
182
+ // Keep client stable (for example with useMemo).
183
+ <AuthLinkPasscode client={client} expiresAt={pending.expiresAt}
184
+ locale="zh-CN" onVerified={updateUI} />
185
+ ```
186
+ ```vue
187
+ <script setup>
188
+ import { AuthLinkPasscode } from '@auth-link/web/vue';
688
189
  </script>
689
-
690
190
  <template>
691
- <button :disabled="busy || !!ticket" @click="start('bind')">Bind phone</button>
692
- <button :disabled="busy || !!ticket" @click="start('confirm')">Confirm</button>
693
- <AuthLinkBind v-if="ticket?.purpose === 'bind'" :ticket="ticket" :client="client" variant="B"
694
- @confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected')"
695
- @expired="finish('Expired; start again')" @error="showError" />
696
- <AuthLinkConfirm v-else-if="ticket?.purpose === 'confirm'" :ticket="ticket" :client="client" variant="B"
697
- @confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected')"
698
- @expired="finish('Expired; start again')" @error="showError" />
699
- <p role="status">{{ message }}</p>
191
+ <AuthLinkPasscode :client="client" :expires-at="pending.expiresAt"
192
+ locale="zh-CN" @verified="updateUI" />
700
193
  </template>
701
194
  ```
702
195
 
703
- ticket は shallowRef、client は markRaw で保持します。同じ用途の作成失敗ではキーを保持し、成功後に消去します。mounted/watch で要求を作りません。確認・拒否・期限切れ・閉じる操作で ticket を消去します。通信エラーでは表示を維持して同じ要求を再試行します。必要なアカウント状態は自サイトの API から再取得します。
704
-
705
- ### 4. React
706
-
707
- 上記で初期化した安定した client と mode=bind/confirm を渡します。ログアウトやアカウント/session 変更ではアンマウントします。
708
-
709
- ```tsx
710
- "use client";
711
- import { useEffect, useRef, useState } from 'react';
712
- import { requestKey } from '@auth-link/web';
713
- import type { AuthLinkClient, Mode, Ticket } from '@auth-link/web';
714
- import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/react';
715
- // Import '@auth-link/web/styles.css' once in your application entry.
716
-
717
- export function AuthLinkPanel({ client, mode }: { client: AuthLinkClient; mode: Mode }) {
718
- const [ticket, setTicket] = useState<Ticket | null>(null);
719
- const [busy, setBusy] = useState(false);
720
- const [message, setMessage] = useState('');
721
- const creation = useRef<{ mode: Mode; key: string } | null>(null);
722
- const inFlight = useRef(false);
723
- const controller = useRef<AbortController | null>(null);
724
- useEffect(() => () => { controller.current?.abort(); }, []);
725
- function finish(text: string) { setTicket(null); setMessage(text); }
726
- async function start() {
727
- if (inFlight.current || ticket) return;
728
- inFlight.current = true; setBusy(true); setMessage('');
729
- const attempt = new AbortController(); controller.current = attempt;
730
- try {
731
- if (creation.current?.mode !== mode) creation.current = { mode, key: requestKey() };
732
- const value = await client.create(mode, { idempotencyKey: creation.current.key, signal: attempt.signal });
733
- if (attempt.signal.aborted) return;
734
- setTicket(value); creation.current = null;
735
- } catch (error) { if (!attempt.signal.aborted) setMessage((error as Error).message); }
736
- finally {
737
- inFlight.current = false;
738
- if (!attempt.signal.aborted) setBusy(false);
739
- }
740
- }
741
- const Component = mode === 'bind' ? AuthLinkBind : AuthLinkConfirm;
742
- return <>
743
- {!ticket && <button disabled={busy} onClick={() => void start()}>Start verification</button>}
744
- {ticket && <Component ticket={ticket} client={client} variant="B"
745
- onConfirmed={result => finish(`${result.purpose}: ${result.requestId}`)}
746
- onClose={() => finish('Closed')} onRejected={() => finish('Rejected')}
747
- onExpired={() => finish('Expired; start again')}
748
- onError={error => setMessage(error.message)} />}
749
- <p role="status">{message}</p>
750
- </>;
751
- }
752
- ```
753
-
754
- ### 5. 通常の JavaScript / 直接表示
755
-
756
- ```ts
757
- import { openAuthLinkConfirm, requestKey } from '@auth-link/web';
758
- import '@auth-link/web/styles.css';
759
-
760
- // Inside a click handler; client is the stable instance created above.
761
- const ticket = await client.create('confirm', { idempotencyKey: requestKey() });
762
- const modal = openAuthLinkConfirm({
763
- ticket, client, variant: 'B', locale: 'zh-CN',
764
- onConfirmed: result => console.log(result.requestId),
765
- onRejected: result => console.log(result.code),
766
- onExpired: () => console.log('Expired'),
767
- onError: error => console.log(error.message),
768
- onClose: () => console.log('Closed'),
769
- });
770
- // Route/component cleanup: modal.destroy();
771
- // Optional: modal.close(); modal.retry(); modal.setLocale('ja'); modal.setVariant('C');
772
- ```
773
-
774
- 登録時は client.create('bind', ...) と openAuthLinkBind(...) を使います。確認・拒否は自動で閉じてから通知し、onClose は発火しません。閉じる操作は監視を停止します。画面を離れる際は destroy を呼びます。同じ要求の成功はインスタンス内で一度だけ通知します。イベントは表示更新用で、保護操作は自サイトの業務 API を呼びます。
775
-
776
- ### 6. フロントエンドが使う API の形式
777
-
778
- | 自サイトのルート | 送信 | 受信 |
779
- | --- | --- | --- |
780
- | POST /api/authlink/requests | purpose=bind/confirm、Idempotency-Key | Ticket |
781
- | POST /api/authlink/requests/{requestId}/result | 空オブジェクト、現在のログイン情報 | Result(自動監視) |
782
-
783
- Cookie モードでは両方に現在の CSRF ヘッダーを送ります。Ticket は requestId、purpose、qr、pairingCode、expiresAt、confirm 時は displayCode も必要です。Ticket/Result/Confirmation 型を import し、QR や数字を自作しません。confirmed のみ成功イベントを発火します。new/pending/approved/consumed は待機、rejected/expired は対応イベントです。
784
-
785
- create() は purpose のみ送ります。業務固有項目が必要なら独自 fetch で作成し、validateTicket で検証して同じコンポーネントへ渡します。post/session/backendOrigin は既存のフロントエンド状態で、同じ入力の再試行では同じキーを保持します。
786
-
787
- ```ts
788
- import { requestKey, validateTicket } from '@auth-link/web';
789
-
790
- // Keep this key across retries of the SAME operation and payload.
791
- const idempotencyKey = requestKey();
792
- const response = await fetch(`${backendOrigin}/api/authlink/requests`, {
793
- method: 'POST', credentials: 'include', cache: 'no-store', redirect: 'error',
794
- headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': session.csrf,
795
- 'Idempotency-Key': idempotencyKey },
796
- body: JSON.stringify({ purpose: 'confirm', postId: post.id, version: post.version }),
797
- });
798
- if (!response.ok) throw new Error('Creation failed');
799
- const ticket = validateTicket(await response.json(), 'confirm', 'https://api.getauthlink.com');
800
- // Pass ticket and the same client to AuthLinkConfirm.
801
- ```
802
-
803
- ### 7. 設定・イベント・表示
804
-
805
- | prop | 型 | 用途 |
806
- | --- | --- | --- |
807
- | `ticket` | `Ticket` | Required; the current request |
808
- | `client` | `AuthLinkClient` | Required; keep the instance stable |
809
- | `locale` | `Locale` | Default `zh-CN` |
810
- | `variant` | `'A' / 'B' / 'C'` | Default `A` |
811
- | `showQr` | `boolean` | Optional QR for confirm; bind always shows QR |
812
- | `intervalMs` | `number` | Default 1500; range 250–10000 ms |
813
- | `onRefresh` | `() => void` | Optional user action after expiry; create a new ticket |
814
- | `className` | `string` | Custom dialog class; Vue also accepts `class` |
815
-
816
- | React コールバック / Vue イベント | 値 | 動作 |
817
- | --- | --- | --- |
818
- | `onConfirmed` / `confirmed` | `Confirmation` | Success; dialog closes automatically |
819
- | `onRejected` / `rejected` | `Rejection` | `{status:'rejected',code:'REJECTED',requestId,purpose}`; dialog closes |
820
- | `onExpired` / `expired` | — | Request expired |
821
- | `onClose` / `close` | — | User closes the dialog; clear ticket |
822
- | `onError` / `error` | `Error` | Read `error.message`; retain the dialog for retry |
823
-
824
- A は中央カード(既定)、B はデスクトップ分割・狭い画面で縦配置、C は小型・モバイルで下部シートです。locale/variant の変更では要求を作り直しません。ネイティブ dialog.showModal() 対応ブラウザーが必要です。
825
-
826
- CSS を明示 import します。ダイアログは document.body 配下なので :root または .authlink-dialog で --authlink-foreground、--authlink-background、--authlink-accent、--authlink-muted を指定します。Vue class は文字列・配列・オブジェクトを className と併用できます。SSR はダイアログや要求を作りません。
827
-
828
- 言語:`zh-CN` (default), `zh-TW`, `en`, `ja`, `bn`, `es`, `fil`, `hi`, `id`, `ms`, `pt`, `th`, `vi`。未指定・未対応値は簡体字中国語へフォールバックします。supportedLocales で一覧を取得できます。
829
-
830
- ### 8. 通信設定とエラー確認
831
-
832
- | 設定 | 用途 |
833
- | --- | --- |
834
- | `baseUrl` | Your application API prefix; default `/api/authlink` |
835
- | `allowedBackendOrigins` | Exact extra application API origins; no paths/wildcards |
836
- | `credentials` | Default `same-origin`; cross-origin cookies need `include` |
837
- | `csrfToken` | Sync/async getter for the current website CSRF token |
838
- | `csrfHeaderName` | Default `X-CSRF-Token` |
839
- | `accessToken` | Sync/async website Bearer token getter, without `Bearer ` |
840
- | `headers` | Extra headers or sync/async getter |
841
- | `authLinkOrigin` | `https://api.getauthlink.com` for production AuthLink |
842
- | `fetch` | Optional fetch implementation |
843
-
844
- 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 秒、閉じる/アンマウントで監視を中止します。
196
+ ### 失敗と終了処理
845
197
 
846
- 期限切れの数字は隠れます。数字をフロントエンドで生成・キャッシュしません。QR はローカル生成で、CSP 使用時は data: 画像を許可します。イベントや localStorage、ブラウザーからの状態値は業務上の許可証ではありません。
198
+ 使用済みコードは別操作や別セッションに使えません。ログインや初回連携直後は次のコードを待ちます。PASSCODE_INVALID、PASSCODE_USED、PASSCODE_LOCKEDを成功として扱わないでください。通信結果不明時は元の冪等キーとコードを保持して再試行し、業務を二重実行しません。
847
199
 
848
- ### ライセンス
200
+ destroy()はブラウザー要求を中止してタイマーを解除し、終了コールバックを呼びません。close()はonCloseも呼びます。画面を閉じてもサーバー処理は元に戻らず、キャンセルと保留中操作はCMS側で管理します。待機やローカル期限判定はポーリングしません。
849
201
 
850
- MIT。LICENSE と THIRD_PARTY_NOTICES.md を参照してください。
202
+ 旧openAuthLink / Ticket / Consumeは以前のプロトコル向けに残ります。新アプリはopenPasscode / AuthLinkPasscodeとサーバーのAuthenticate / authenticateを使います。旧連携の移行機能は提供しません。