@auth-link/web 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +581 -1783
- package/docs/README.en.md +200 -323
- package/docs/README.ja.md +200 -323
- package/docs/README.zh-CN.md +201 -394
- package/examples/react-client.tsx +42 -0
- package/examples/vue-client.vue +55 -0
- package/package.json +6 -3
- package/docs/go-vue.en.md +0 -254
- package/docs/go-vue.ja.md +0 -254
- package/docs/go-vue.zh-CN.md +0 -258
- package/examples/vue-cms.vue +0 -109
package/docs/README.en.md
CHANGED
|
@@ -2,404 +2,281 @@
|
|
|
2
2
|
|
|
3
3
|
**English** | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
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.
|
|
6
6
|
|
|
7
|
-
##
|
|
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
|
-
|
|
13
|
-
## Features
|
|
14
|
-
|
|
15
|
-
- Phone-binding QR codes generated locally in the browser.
|
|
16
|
-
- Matching numbers that update every 30 seconds, synchronized with the AuthLink app.
|
|
17
|
-
- Confirmation, rejection, expiry and error callbacks.
|
|
18
|
-
- React components and hook, Vue components and composable, and framework-independent tools.
|
|
19
|
-
- 13 interface languages, **Simplified Chinese by default**, with per-component language selection.
|
|
20
|
-
- TypeScript declarations, separate CSS and optional React/Vue peer dependencies.
|
|
21
|
-
|
|
22
|
-
This is a browser integration library. It works with your application backend, the AuthLink service and the mobile app. The service verifies the original phone’s hardware-backed proof after the user approves in the app. Screen-lock checks and system passkeys are not required. Installing the library alone does not create an authentication service.
|
|
23
|
-
|
|
24
|
-
## Install
|
|
7
|
+
## 1. Install and distinguish the two API origins
|
|
25
8
|
|
|
26
9
|
```sh
|
|
27
|
-
npm install @auth-link/web
|
|
10
|
+
npm install @auth-link/web@latest
|
|
11
|
+
# Also install the framework your project uses: vue, or react / react-dom.
|
|
28
12
|
```
|
|
29
13
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
1. Configure your backend's AuthLink tenant credentials.
|
|
35
|
-
2. Implement the two application backend endpoints below (same-origin or explicitly allowed cross-origin), authenticated to the current website user.
|
|
36
|
-
3. Create a request on a user action and pass its ticket to a component.
|
|
37
|
-
4. Your backend verifies and consumes the result before returning a confirmed response.
|
|
38
|
-
|
|
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.
|
|
14
|
+
| Origin | Frontend option | Purpose |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| Your application API, e.g. `https://cms-api.example.com` | baseUrl, allowedBackendOrigins | Session, request creation and results |
|
|
17
|
+
| AuthLink production API `https://api.getauthlink.com` | authLinkOrigin | Validate the AuthLink origin in the ticket |
|
|
46
18
|
|
|
47
|
-
|
|
48
|
-
<meta name="csrf-token" content="{{ csrfToken }}">
|
|
49
|
-
```
|
|
19
|
+
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.
|
|
50
20
|
|
|
51
|
-
The
|
|
21
|
+
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.
|
|
52
22
|
|
|
53
|
-
|
|
23
|
+
## 2. Load the current session and initialize a client
|
|
54
24
|
|
|
55
25
|
```ts
|
|
56
|
-
// authlink-client.ts
|
|
57
26
|
import { createAuthLinkClient } from '@auth-link/web';
|
|
58
27
|
import '@auth-link/web/styles.css';
|
|
59
28
|
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
export const client = createAuthLinkClient({
|
|
69
|
-
baseUrl: '/api/authlink',
|
|
70
|
-
csrfToken: readCsrfTokenFromYourCms,
|
|
71
|
-
// Replace this example with the configured AuthLink service origin.
|
|
72
|
-
authLinkOrigin: 'https://auth.example.com',
|
|
73
|
-
});
|
|
74
|
-
```
|
|
75
|
-
|
|
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',
|
|
29
|
+
// getCsrf reads your website's CURRENT session token on every request.
|
|
30
|
+
export function createWebsiteClient(getCsrf: () => string, backendOrigin?: string) {
|
|
31
|
+
return createAuthLinkClient({
|
|
32
|
+
baseUrl: backendOrigin ? `${backendOrigin}/api/authlink` : '/api/authlink',
|
|
33
|
+
allowedBackendOrigins: backendOrigin ? [backendOrigin] : [],
|
|
34
|
+
credentials: backendOrigin ? 'include' : 'same-origin',
|
|
35
|
+
csrfToken: getCsrf,
|
|
36
|
+
authLinkOrigin: 'https://api.getauthlink.com',
|
|
97
37
|
});
|
|
98
|
-
if (!response.ok) throw new Error('Session initialization failed');
|
|
99
|
-
const session: unknown = await response.json();
|
|
100
|
-
if (!session || typeof session !== 'object' || !('csrfToken' in session) ||
|
|
101
|
-
typeof session.csrfToken !== 'string' || !session.csrfToken) {
|
|
102
|
-
throw new Error('The session endpoint must return { csrfToken: string }');
|
|
103
|
-
}
|
|
104
|
-
let currentCsrfToken = session.csrfToken;
|
|
105
|
-
return {
|
|
106
|
-
client: createAuthLinkClient({
|
|
107
|
-
baseUrl: `${backendOrigin}/api/authlink`,
|
|
108
|
-
allowedBackendOrigins: [backendOrigin],
|
|
109
|
-
credentials: 'include',
|
|
110
|
-
csrfToken: () => currentCsrfToken,
|
|
111
|
-
}),
|
|
112
|
-
// Call after your application's session/token refresh; recreate on account changes.
|
|
113
|
-
updateCsrfToken(value: string) { currentCsrfToken = value; },
|
|
114
|
-
};
|
|
115
38
|
}
|
|
116
39
|
```
|
|
117
40
|
|
|
118
|
-
After login,
|
|
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.
|
|
41
|
+
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.
|
|
123
42
|
|
|
124
43
|
```ts
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
});
|
|
44
|
+
// Run in your website's existing authenticated browser flow.
|
|
45
|
+
const backendOrigin = 'https://cms-api.example.com'; // YOUR application backend
|
|
46
|
+
const response = await fetch(`${backendOrigin}/api/session`, {
|
|
47
|
+
credentials: 'include', cache: 'no-store', redirect: 'error',
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) throw new Error('Session unavailable');
|
|
50
|
+
let session = await response.json();
|
|
51
|
+
if (session.authenticated !== true || typeof session.csrf !== 'string' || !session.csrf) {
|
|
52
|
+
throw new Error('Log in to your website first');
|
|
135
53
|
}
|
|
54
|
+
const client = createWebsiteClient(() => session.csrf, backendOrigin);
|
|
55
|
+
// Same origin: createWebsiteClient(() => session.csrf)
|
|
56
|
+
// After token refresh, replace session or update session.csrf.
|
|
57
|
+
// On logout/account/session change, close/unmount old dialogs and recreate client state.
|
|
136
58
|
```
|
|
137
59
|
|
|
138
|
-
|
|
60
|
+
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.
|
|
139
61
|
|
|
140
|
-
|
|
62
|
+
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.
|
|
141
63
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
Vary: Origin
|
|
64
|
+
```ts
|
|
65
|
+
const backendOrigin = 'https://cms-api.example.com'; // YOUR application backend
|
|
66
|
+
const client = createAuthLinkClient({
|
|
67
|
+
baseUrl: `${backendOrigin}/api/authlink`, allowedBackendOrigins: [backendOrigin],
|
|
68
|
+
credentials: 'omit', accessToken: () => auth.accessToken,
|
|
69
|
+
authLinkOrigin: 'https://api.getauthlink.com',
|
|
70
|
+
});
|
|
150
71
|
```
|
|
151
72
|
|
|
152
|
-
|
|
73
|
+
## 3. Vue: create on click, display and clean up
|
|
153
74
|
|
|
154
|
-
|
|
75
|
+
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.
|
|
155
76
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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.
|
|
171
|
-
|
|
172
|
-
## Open a dialog directly (React, Vue or plain JavaScript)
|
|
173
|
-
|
|
174
|
-
```ts
|
|
175
|
-
import { openAuthLinkBind, openAuthLinkConfirm, requestKey } from '@auth-link/web';
|
|
176
|
-
import { client } from './authlink-client';
|
|
77
|
+
```vue
|
|
78
|
+
<script setup lang="ts">
|
|
79
|
+
import { markRaw, onUnmounted, ref, shallowRef } from 'vue';
|
|
80
|
+
import { createAuthLinkClient, requestKey } from '@auth-link/web';
|
|
81
|
+
import type { Confirmation, Mode, Ticket } from '@auth-link/web';
|
|
82
|
+
import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/vue';
|
|
177
83
|
import '@auth-link/web/styles.css';
|
|
178
84
|
|
|
179
|
-
//
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
85
|
+
// csrf comes from your website's current authenticated session.
|
|
86
|
+
const props = defineProps<{ csrf: string; backendOrigin?: string }>();
|
|
87
|
+
const ticket = shallowRef<Ticket | null>(null);
|
|
88
|
+
const busy = ref(false), message = ref('');
|
|
89
|
+
const lifetime = new AbortController();
|
|
90
|
+
let creationKey: string | null = null;
|
|
91
|
+
let creationMode: Mode | null = null;
|
|
92
|
+
const client = markRaw(createAuthLinkClient({
|
|
93
|
+
baseUrl: props.backendOrigin ? `${props.backendOrigin}/api/authlink` : '/api/authlink',
|
|
94
|
+
allowedBackendOrigins: props.backendOrigin ? [props.backendOrigin] : [],
|
|
95
|
+
credentials: props.backendOrigin ? 'include' : 'same-origin',
|
|
96
|
+
csrfToken: () => props.csrf,
|
|
97
|
+
authLinkOrigin: 'https://api.getauthlink.com',
|
|
98
|
+
}));
|
|
99
|
+
function finish(text: string) { ticket.value = null; message.value = text; }
|
|
100
|
+
function confirmed(result: Confirmation) {
|
|
101
|
+
finish(`${result.purpose}: ${result.requestId}`);
|
|
102
|
+
// Reload your website's account/status API if the page needs fresh state.
|
|
103
|
+
}
|
|
104
|
+
function showError(error: Error) { message.value = error.message; }
|
|
105
|
+
async function start(mode: Mode) {
|
|
106
|
+
if (busy.value || ticket.value) return;
|
|
107
|
+
busy.value = true; message.value = '';
|
|
108
|
+
try {
|
|
109
|
+
if (creationMode !== mode) { creationKey = null; creationMode = mode; }
|
|
110
|
+
creationKey ??= requestKey();
|
|
111
|
+
const value = await client.create(mode, { idempotencyKey: creationKey, signal: lifetime.signal });
|
|
112
|
+
if (lifetime.signal.aborted) return;
|
|
113
|
+
ticket.value = value; creationKey = null;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (!lifetime.signal.aborted) showError(error as Error);
|
|
116
|
+
// Keep the same key when retrying an uncertain creation of the same mode.
|
|
117
|
+
} finally { busy.value = false; }
|
|
118
|
+
}
|
|
119
|
+
onUnmounted(() => { lifetime.abort(); ticket.value = null; creationKey = null; });
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<template>
|
|
123
|
+
<button :disabled="busy || !!ticket" @click="start('bind')">Bind phone</button>
|
|
124
|
+
<button :disabled="busy || !!ticket" @click="start('confirm')">Confirm</button>
|
|
125
|
+
<AuthLinkBind v-if="ticket?.purpose === 'bind'" :ticket="ticket" :client="client" variant="B"
|
|
126
|
+
@confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected')"
|
|
127
|
+
@expired="finish('Expired; start again')" @error="showError" />
|
|
128
|
+
<AuthLinkConfirm v-else-if="ticket?.purpose === 'confirm'" :ticket="ticket" :client="client" variant="B"
|
|
129
|
+
@confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected')"
|
|
130
|
+
@expired="finish('Expired; start again')" @error="showError" />
|
|
131
|
+
<p role="status">{{ message }}</p>
|
|
132
|
+
</template>
|
|
189
133
|
```
|
|
190
134
|
|
|
191
|
-
|
|
135
|
+
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.
|
|
192
136
|
|
|
193
|
-
## React
|
|
137
|
+
## 4. React
|
|
194
138
|
|
|
195
|
-
|
|
139
|
+
Pass a stable client created as above and mode=bind or confirm. Unmount the instance on logout/account/session changes.
|
|
196
140
|
|
|
197
141
|
```tsx
|
|
198
142
|
"use client";
|
|
199
|
-
import { useRef, useState } from 'react';
|
|
143
|
+
import { useEffect, useRef, useState } from 'react';
|
|
200
144
|
import { requestKey } from '@auth-link/web';
|
|
201
|
-
import type { Mode, Ticket } from '@auth-link/web';
|
|
145
|
+
import type { AuthLinkClient, Mode, Ticket } from '@auth-link/web';
|
|
202
146
|
import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/react';
|
|
203
|
-
|
|
147
|
+
// Import '@auth-link/web/styles.css' once in your application entry.
|
|
204
148
|
|
|
205
|
-
export function AuthLinkPanel({ mode }: { mode: Mode }) {
|
|
149
|
+
export function AuthLinkPanel({ client, mode }: { client: AuthLinkClient; mode: Mode }) {
|
|
206
150
|
const [ticket, setTicket] = useState<Ticket | null>(null);
|
|
207
151
|
const [busy, setBusy] = useState(false);
|
|
208
152
|
const [message, setMessage] = useState('');
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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('');
|
|
153
|
+
const creation = useRef<{ mode: Mode; key: string } | null>(null);
|
|
154
|
+
const inFlight = useRef(false);
|
|
155
|
+
const controller = useRef<AbortController | null>(null);
|
|
156
|
+
useEffect(() => () => { controller.current?.abort(); }, []);
|
|
157
|
+
function finish(text: string) { setTicket(null); setMessage(text); }
|
|
158
|
+
async function start() {
|
|
159
|
+
if (inFlight.current || ticket) return;
|
|
160
|
+
inFlight.current = true; setBusy(true); setMessage('');
|
|
161
|
+
const attempt = new AbortController(); controller.current = attempt;
|
|
227
162
|
try {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
163
|
+
if (creation.current?.mode !== mode) creation.current = { mode, key: requestKey() };
|
|
164
|
+
const value = await client.create(mode, { idempotencyKey: creation.current.key, signal: attempt.signal });
|
|
165
|
+
if (attempt.signal.aborted) return;
|
|
166
|
+
setTicket(value); creation.current = null;
|
|
167
|
+
} catch (error) { if (!attempt.signal.aborted) setMessage((error as Error).message); }
|
|
168
|
+
finally {
|
|
169
|
+
inFlight.current = false;
|
|
170
|
+
if (!attempt.signal.aborted) setBusy(false);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
234
173
|
const Component = mode === 'bind' ? AuthLinkBind : AuthLinkConfirm;
|
|
235
174
|
return <>
|
|
236
|
-
{!ticket && <button
|
|
237
|
-
{ticket && <Component ticket={ticket} client={client}
|
|
238
|
-
onConfirmed={result => result.purpose
|
|
239
|
-
onClose={() => finish('Closed')}
|
|
240
|
-
|
|
241
|
-
onExpired={() => finish('Request expired; start again')}
|
|
175
|
+
{!ticket && <button disabled={busy} onClick={() => void start()}>Start verification</button>}
|
|
176
|
+
{ticket && <Component ticket={ticket} client={client} variant="B"
|
|
177
|
+
onConfirmed={result => finish(`${result.purpose}: ${result.requestId}`)}
|
|
178
|
+
onClose={() => finish('Closed')} onRejected={() => finish('Rejected')}
|
|
179
|
+
onExpired={() => finish('Expired; start again')}
|
|
242
180
|
onError={error => setMessage(error.message)} />}
|
|
243
181
|
<p role="status">{message}</p>
|
|
244
182
|
</>;
|
|
245
183
|
}
|
|
246
184
|
```
|
|
247
185
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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.
|
|
252
|
-
|
|
253
|
-
## Vue
|
|
254
|
-
|
|
255
|
-
```vue
|
|
256
|
-
<script setup lang="ts">
|
|
257
|
-
import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/vue';
|
|
258
|
-
import type { Ticket, Confirmation } from '@auth-link/web';
|
|
259
|
-
import { client } from './authlink-client';
|
|
260
|
-
|
|
261
|
-
defineProps<{ ticket: Ticket }>();
|
|
262
|
-
function confirmed(result: Confirmation) {
|
|
263
|
-
console.log('Confirmed request:', result.requestId);
|
|
264
|
-
}
|
|
265
|
-
</script>
|
|
266
|
-
|
|
267
|
-
<template>
|
|
268
|
-
<AuthLinkBind v-if="ticket.purpose === 'bind'" :ticket="ticket" :client="client"
|
|
269
|
-
@confirmed="confirmed" />
|
|
270
|
-
<AuthLinkConfirm v-else :ticket="ticket" :client="client" locale="ja"
|
|
271
|
-
@confirmed="confirmed" />
|
|
272
|
-
</template>
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
Use `:locale="selectedLanguage"` to switch languages without creating a new verification request. Mounting opens the dialog. Use `@close` to clear the selected ticket. Vue exposes `confirmed`, `rejected`, `expired`, `error` and `close` events.
|
|
276
|
-
|
|
277
|
-
## Choose a layout
|
|
278
|
-
|
|
279
|
-
Set `variant` when opening a dialog or mounting a component. Omit it for **A**.
|
|
280
|
-
|
|
281
|
-
| Value | Desktop | Mobile |
|
|
282
|
-
| --- | --- | --- |
|
|
283
|
-
| `A` (default) | Minimal centered card with a thin code-refresh progress bar | Centered card |
|
|
284
|
-
| `B` | QR and matching code in two columns | Vertical layout |
|
|
285
|
-
| `C` | Compact card with footer cancellation | Bottom sheet with safe-area padding |
|
|
186
|
+
## 5. Plain JavaScript / direct dialogs
|
|
286
187
|
|
|
287
188
|
```ts
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
// modal.setVariant('C'); // Presentation only; preserves the request and polling.
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
```tsx
|
|
294
|
-
<AuthLinkBind ticket={ticket} client={client} variant="C" />
|
|
295
|
-
```
|
|
189
|
+
import { openAuthLinkConfirm, requestKey } from '@auth-link/web';
|
|
190
|
+
import '@auth-link/web/styles.css';
|
|
296
191
|
|
|
297
|
-
|
|
298
|
-
|
|
192
|
+
// Inside a click handler; client is the stable instance created above.
|
|
193
|
+
const ticket = await client.create('confirm', { idempotencyKey: requestKey() });
|
|
194
|
+
const modal = openAuthLinkConfirm({
|
|
195
|
+
ticket, client, variant: 'B', locale: 'zh-CN',
|
|
196
|
+
onConfirmed: result => console.log(result.requestId),
|
|
197
|
+
onRejected: result => console.log(result.code),
|
|
198
|
+
onExpired: () => console.log('Expired'),
|
|
199
|
+
onError: error => console.log(error.message),
|
|
200
|
+
onClose: () => console.log('Closed'),
|
|
201
|
+
});
|
|
202
|
+
// Route/component cleanup: modal.destroy();
|
|
203
|
+
// Optional: modal.close(); modal.retry(); modal.setLocale('ja'); modal.setVariant('C');
|
|
299
204
|
```
|
|
300
205
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
All variants use the same verification, expiry and callbacks. `variant` changes can be reactive in React and Vue without restarting observation. Invalid runtime values fall back to A. The footer Cancel control in B/C has the same behavior as the close icon: it closes observation, without rejecting the server request. Confirmation still dismisses the dialog automatically. A uses a subtle close icon; all close controls retain a 44px hit target and keyboard focus indication.
|
|
206
|
+
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.
|
|
304
207
|
|
|
305
|
-
##
|
|
208
|
+
## 6. API shapes the frontend needs
|
|
306
209
|
|
|
307
|
-
|
|
|
308
|
-
| --- | --- | --- |
|
|
309
|
-
| `ticket` | `Ticket`, required | Existing request returned by your backend |
|
|
310
|
-
| `client` | `AuthLinkClient`, required | Client that queries your backend |
|
|
311
|
-
| `locale` | `Locale` | Interface language; defaults to `zh-CN` |
|
|
312
|
-
| `variant` | `DialogVariant` | `A` (default), `B` or `C`; changes layout only |
|
|
313
|
-
| `showQr` | `boolean` | Optional QR on confirmation panels; binding panels always show it |
|
|
314
|
-
| `intervalMs` | `number` | Polling interval, 250–10000 ms; default 1500 ms |
|
|
315
|
-
| `onRefresh` | `() => void` | Create a new request from the expiry panel and replace the ticket |
|
|
316
|
-
|
|
317
|
-
| React callback / Vue event | Argument | Meaning |
|
|
210
|
+
| Application route | Frontend sends | Frontend reads |
|
|
318
211
|
| --- | --- | --- |
|
|
319
|
-
|
|
|
320
|
-
|
|
|
321
|
-
| `onExpired` / `expired` | None | Request expired |
|
|
322
|
-
| `onClose` / `close` | None | Dialog closed; unmount the component / clear its ticket |
|
|
323
|
-
| `onError` / `error` | `Error` | Network, response validation or callback failure |
|
|
212
|
+
| POST /api/authlink/requests | purpose=bind or confirm; Idempotency-Key | Ticket |
|
|
213
|
+
| POST /api/authlink/requests/{requestId}/result | empty object; current login credentials | Result; polled automatically |
|
|
324
214
|
|
|
325
|
-
|
|
215
|
+
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.
|
|
326
216
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
## Backend contract
|
|
330
|
-
|
|
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.
|
|
217
|
+
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.
|
|
332
218
|
|
|
333
219
|
```ts
|
|
334
|
-
import
|
|
335
|
-
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
"status": "confirmed",
|
|
348
|
-
"requestId": "<request-id>",
|
|
349
|
-
"verificationId": "<verification-id>",
|
|
350
|
-
"purpose": "confirm",
|
|
351
|
-
"authenticationPolicy": "device-presence.v1",
|
|
352
|
-
"expiresAt": "<same-expiry-as-ticket>"
|
|
353
|
-
}
|
|
220
|
+
import { requestKey, validateTicket } from '@auth-link/web';
|
|
221
|
+
|
|
222
|
+
// Keep this key across retries of the SAME operation and payload.
|
|
223
|
+
const idempotencyKey = requestKey();
|
|
224
|
+
const response = await fetch(`${backendOrigin}/api/authlink/requests`, {
|
|
225
|
+
method: 'POST', credentials: 'include', cache: 'no-store', redirect: 'error',
|
|
226
|
+
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': session.csrf,
|
|
227
|
+
'Idempotency-Key': idempotencyKey },
|
|
228
|
+
body: JSON.stringify({ purpose: 'confirm', postId: post.id, version: post.version }),
|
|
229
|
+
});
|
|
230
|
+
if (!response.ok) throw new Error('Creation failed');
|
|
231
|
+
const ticket = validateTicket(await response.json(), 'confirm', 'https://api.getauthlink.com');
|
|
232
|
+
// Pass ticket and the same client to AuthLinkConfirm.
|
|
354
233
|
```
|
|
355
234
|
|
|
356
|
-
|
|
357
|
-
|
|
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.
|
|
359
|
-
|
|
360
|
-
## Languages
|
|
235
|
+
## 7. Props, events and presentation
|
|
361
236
|
|
|
362
|
-
|
|
|
363
|
-
| --- | --- |
|
|
364
|
-
| `
|
|
365
|
-
| `
|
|
366
|
-
| `
|
|
367
|
-
| `
|
|
368
|
-
| `
|
|
369
|
-
| `
|
|
370
|
-
| `
|
|
371
|
-
| `
|
|
372
|
-
|
|
373
|
-
|
|
|
374
|
-
|
|
|
375
|
-
| `
|
|
376
|
-
| `
|
|
237
|
+
| Prop | Type | Meaning |
|
|
238
|
+
| --- | --- | --- |
|
|
239
|
+
| `ticket` | `Ticket` | Required; the current request |
|
|
240
|
+
| `client` | `AuthLinkClient` | Required; keep the instance stable |
|
|
241
|
+
| `locale` | `Locale` | Default `zh-CN` |
|
|
242
|
+
| `variant` | `'A' / 'B' / 'C'` | Default `A` |
|
|
243
|
+
| `showQr` | `boolean` | Optional QR for confirm; bind always shows QR |
|
|
244
|
+
| `intervalMs` | `number` | Default 1500; range 250–10000 ms |
|
|
245
|
+
| `onRefresh` | `() => void` | Optional user action after expiry; create a new ticket |
|
|
246
|
+
| `className` | `string` | Custom dialog class; Vue also accepts `class` |
|
|
247
|
+
|
|
248
|
+
| React callback / Vue event | Value | Behavior |
|
|
249
|
+
| --- | --- | --- |
|
|
250
|
+
| `onConfirmed` / `confirmed` | `Confirmation` | Success; dialog closes automatically |
|
|
251
|
+
| `onRejected` / `rejected` | `Rejection` | `{status:'rejected',code:'REJECTED',requestId,purpose}`; dialog closes |
|
|
252
|
+
| `onExpired` / `expired` | — | Request expired |
|
|
253
|
+
| `onClose` / `close` | — | User closes the dialog; clear ticket |
|
|
254
|
+
| `onError` / `error` | `Error` | Read `error.message`; retain the dialog for retry |
|
|
377
255
|
|
|
378
|
-
|
|
256
|
+
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.
|
|
379
257
|
|
|
380
|
-
|
|
258
|
+
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.
|
|
381
259
|
|
|
382
|
-
|
|
260
|
+
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.
|
|
383
261
|
|
|
384
|
-
|
|
262
|
+
## 8. Transport options and troubleshooting
|
|
385
263
|
|
|
386
|
-
|
|
|
264
|
+
| Option | Purpose |
|
|
387
265
|
| --- | --- |
|
|
388
|
-
|
|
|
389
|
-
|
|
|
390
|
-
|
|
|
391
|
-
|
|
|
266
|
+
| `baseUrl` | Your application API prefix; default `/api/authlink` |
|
|
267
|
+
| `allowedBackendOrigins` | Exact extra application API origins; no paths/wildcards |
|
|
268
|
+
| `credentials` | Default `same-origin`; cross-origin cookies need `include` |
|
|
269
|
+
| `csrfToken` | Sync/async getter for the current website CSRF token |
|
|
270
|
+
| `csrfHeaderName` | Default `X-CSRF-Token` |
|
|
271
|
+
| `accessToken` | Sync/async website Bearer token getter, without `Bearer ` |
|
|
272
|
+
| `headers` | Extra headers or sync/async getter |
|
|
273
|
+
| `authLinkOrigin` | `https://api.getauthlink.com` for production AuthLink |
|
|
274
|
+
| `fetch` | Optional fetch implementation |
|
|
392
275
|
|
|
393
|
-
|
|
276
|
+
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.
|
|
394
277
|
|
|
395
|
-
|
|
396
|
-
- The browser receives only public tickets and current display frames, never generation seeds, device keys or tenant secrets.
|
|
397
|
-
- A request lasts at most 120 seconds; rotating the code does not extend it. Backend and app must support AuthLink mobile protocol 7.
|
|
398
|
-
- Countdown uses server time and a monotonic clock. Expired numbers are hidden even if a network call stalls; a short update gap at rotation is expected.
|
|
399
|
-
- Component unmount/request changes abort polling. Confirmation is deduplicated per mounted instance; your backend still needs persistent idempotence.
|
|
400
|
-
- Frontend callbacks update UI. Protected APIs must rely on trusted backend records, never a JavaScript callback or a browser-supplied success flag.
|
|
401
|
-
- QR generation is local; backend SVG is not injected and no third-party QR service receives the ticket. Allow `img-src 'self' data:` in your CSP.
|
|
278
|
+
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.
|
|
402
279
|
|
|
403
280
|
## License
|
|
404
281
|
|
|
405
|
-
MIT. See
|
|
282
|
+
MIT. See LICENSE and THIRD_PARTY_NOTICES.md.
|