@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/CHANGELOG.md +27 -0
- package/README.md +111 -759
- package/dist/index.js +466 -178
- package/dist/index.js.map +3 -3
- package/dist/react.js +466 -177
- package/dist/react.js.map +3 -3
- package/dist/styles.css +11 -49
- package/dist/types/core.d.ts +2 -0
- package/dist/types/i18n.d.ts +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/modal.d.ts +3 -3
- package/dist/types/passcode.d.ts +28 -0
- package/dist/types/react.d.ts +5 -1
- package/dist/types/types.d.ts +2 -4
- package/dist/types/view.d.ts +5 -4
- package/dist/types/vue.d.ts +5 -1
- package/dist/vue.js +479 -176
- package/dist/vue.js.map +3 -3
- package/docs/README.en.md +39 -255
- package/docs/README.ja.md +39 -255
- package/docs/README.zh-CN.md +39 -255
- package/examples/react-client.tsx +16 -34
- package/examples/vue-client.vue +22 -46
- package/package.json +2 -2
package/docs/README.en.md
CHANGED
|
@@ -1,282 +1,66 @@
|
|
|
1
|
-
# AuthLink Web
|
|
1
|
+
# AuthLink Web 2.0
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](README.en.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Upgrading from 1.x
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
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.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
npm install @auth-link/web@latest
|
|
11
|
-
# Also install the framework your project uses: vue, or react / react-dom.
|
|
12
|
-
```
|
|
13
|
-
|
|
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 |
|
|
18
|
-
|
|
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.
|
|
9
|
+
## Direct rotating-code verification
|
|
20
10
|
|
|
21
|
-
The
|
|
11
|
+
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.
|
|
22
12
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```ts
|
|
26
|
-
import { createAuthLinkClient } from '@auth-link/web';
|
|
27
|
-
import '@auth-link/web/styles.css';
|
|
13
|
+
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.
|
|
28
14
|
|
|
29
|
-
|
|
30
|
-
|
|
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',
|
|
37
|
-
});
|
|
38
|
-
}
|
|
15
|
+
```sh
|
|
16
|
+
npm install @auth-link/web@^2.0.0
|
|
39
17
|
```
|
|
40
18
|
|
|
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.
|
|
42
|
-
|
|
43
19
|
```ts
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
20
|
+
import { openPasscode, createPasscodeClient } from '@auth-link/web';
|
|
21
|
+
import '@auth-link/web/styles.css';
|
|
22
|
+
const client = createPasscodeClient({ baseUrl: '/api', csrfToken: () => csrfToken });
|
|
23
|
+
const modal = openPasscode({
|
|
24
|
+
client, expiresAt: pending.expiresAt, locale: 'zh-CN',
|
|
25
|
+
// First-time enrollment only:
|
|
26
|
+
// bindingQr: pending.bindingQr, authLinkOrigin: 'https://api.getauthlink.com',
|
|
27
|
+
onVerified: result => updateUI(result.verificationId),
|
|
48
28
|
});
|
|
49
|
-
|
|
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');
|
|
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.
|
|
29
|
+
// modal.setLocale('en'); modal.destroy();
|
|
58
30
|
```
|
|
59
31
|
|
|
60
|
-
|
|
32
|
+
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.
|
|
61
33
|
|
|
62
|
-
|
|
34
|
+
## Options and callbacks
|
|
63
35
|
|
|
64
|
-
|
|
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
|
-
});
|
|
71
|
-
```
|
|
36
|
+
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.
|
|
72
37
|
|
|
73
|
-
|
|
38
|
+
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.
|
|
74
39
|
|
|
75
|
-
|
|
40
|
+
## React and Vue
|
|
76
41
|
|
|
77
|
-
|
|
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';
|
|
83
|
-
import '@auth-link/web/styles.css';
|
|
42
|
+
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.
|
|
84
43
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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; });
|
|
44
|
+
```tsx
|
|
45
|
+
import { AuthLinkPasscode } from '@auth-link/web/react';
|
|
46
|
+
// Keep client stable (for example with useMemo).
|
|
47
|
+
<AuthLinkPasscode client={client} expiresAt={pending.expiresAt}
|
|
48
|
+
locale="zh-CN" onVerified={updateUI} />
|
|
49
|
+
```
|
|
50
|
+
```vue
|
|
51
|
+
<script setup>
|
|
52
|
+
import { AuthLinkPasscode } from '@auth-link/web/vue';
|
|
120
53
|
</script>
|
|
121
|
-
|
|
122
54
|
<template>
|
|
123
|
-
<
|
|
124
|
-
|
|
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>
|
|
55
|
+
<AuthLinkPasscode :client="client" :expires-at="pending.expiresAt"
|
|
56
|
+
locale="zh-CN" @verified="updateUI" />
|
|
132
57
|
</template>
|
|
133
58
|
```
|
|
134
59
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
## 4. React
|
|
138
|
-
|
|
139
|
-
Pass a stable client created as above and mode=bind or confirm. Unmount the instance on logout/account/session changes.
|
|
140
|
-
|
|
141
|
-
```tsx
|
|
142
|
-
"use client";
|
|
143
|
-
import { useEffect, useRef, useState } from 'react';
|
|
144
|
-
import { requestKey } from '@auth-link/web';
|
|
145
|
-
import type { AuthLinkClient, Mode, Ticket } from '@auth-link/web';
|
|
146
|
-
import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/react';
|
|
147
|
-
// Import '@auth-link/web/styles.css' once in your application entry.
|
|
148
|
-
|
|
149
|
-
export function AuthLinkPanel({ client, mode }: { client: AuthLinkClient; mode: Mode }) {
|
|
150
|
-
const [ticket, setTicket] = useState<Ticket | null>(null);
|
|
151
|
-
const [busy, setBusy] = useState(false);
|
|
152
|
-
const [message, setMessage] = useState('');
|
|
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;
|
|
162
|
-
try {
|
|
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
|
-
}
|
|
173
|
-
const Component = mode === 'bind' ? AuthLinkBind : AuthLinkConfirm;
|
|
174
|
-
return <>
|
|
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')}
|
|
180
|
-
onError={error => setMessage(error.message)} />}
|
|
181
|
-
<p role="status">{message}</p>
|
|
182
|
-
</>;
|
|
183
|
-
}
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
## 5. Plain JavaScript / direct dialogs
|
|
187
|
-
|
|
188
|
-
```ts
|
|
189
|
-
import { openAuthLinkConfirm, requestKey } from '@auth-link/web';
|
|
190
|
-
import '@auth-link/web/styles.css';
|
|
191
|
-
|
|
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');
|
|
204
|
-
```
|
|
205
|
-
|
|
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.
|
|
207
|
-
|
|
208
|
-
## 6. API shapes the frontend needs
|
|
209
|
-
|
|
210
|
-
| Application route | Frontend sends | Frontend reads |
|
|
211
|
-
| --- | --- | --- |
|
|
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 |
|
|
214
|
-
|
|
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.
|
|
216
|
-
|
|
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.
|
|
218
|
-
|
|
219
|
-
```ts
|
|
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.
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
## 7. Props, events and presentation
|
|
236
|
-
|
|
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 |
|
|
255
|
-
|
|
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.
|
|
257
|
-
|
|
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.
|
|
259
|
-
|
|
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.
|
|
261
|
-
|
|
262
|
-
## 8. Transport options and troubleshooting
|
|
263
|
-
|
|
264
|
-
| Option | Purpose |
|
|
265
|
-
| --- | --- |
|
|
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 |
|
|
275
|
-
|
|
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.
|
|
60
|
+
## Errors and cleanup
|
|
277
61
|
|
|
278
|
-
|
|
62
|
+
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.
|
|
279
63
|
|
|
280
|
-
|
|
64
|
+
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.
|
|
281
65
|
|
|
282
|
-
|
|
66
|
+
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.
|