@auth-link/web 0.0.5 → 1.1.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/docs/README.en.md CHANGED
@@ -1,282 +1,62 @@
1
- # AuthLink Web
1
+ # AuthLink Web 1.1
2
2
 
3
- **English** | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md)
3
+ [English](README.en.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md)
4
4
 
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.
5
+ ## Direct rotating-code verification
6
6
 
7
- ## 1. Install and distinguish the two API origins
7
+ 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.
8
+
9
+ 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.
8
10
 
9
11
  ```sh
10
- npm install @auth-link/web@latest
11
- # Also install the framework your project uses: vue, or react / react-dom.
12
+ npm install @auth-link/web@^1.1.0
12
13
  ```
13
14
 
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.
20
-
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.
22
-
23
- ## 2. Load the current session and initialize a client
24
-
25
15
  ```ts
26
- import { createAuthLinkClient } from '@auth-link/web';
16
+ import { openPasscode, createPasscodeClient } from '@auth-link/web';
27
17
  import '@auth-link/web/styles.css';
28
-
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',
37
- });
38
- }
39
- ```
40
-
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
- ```ts
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',
18
+ const client = createPasscodeClient({ baseUrl: '/api', csrfToken: () => csrfToken });
19
+ const modal = openPasscode({
20
+ client, expiresAt: pending.expiresAt, locale: 'zh-CN', variant: 'B',
21
+ // First-time enrollment only:
22
+ // bindingQr: pending.bindingQr, authLinkOrigin: 'https://api.getauthlink.com',
23
+ onVerified: result => updateUI(result.verificationId),
48
24
  });
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');
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.
25
+ // modal.setLocale('en'); modal.setVariant('C'); modal.destroy();
58
26
  ```
59
27
 
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.
28
+ 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
29
 
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.
30
+ ## Options and callbacks
63
31
 
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
- });
71
- ```
32
+ Required: client.verify(passcode, signal) and expiresAt (the pending CMS action deadline). Optional: locale, variant A/B/C, 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
33
 
73
- ## 3. Vue: create on click, display and clean up
34
+ 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
35
 
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.
36
+ ## React and Vue
76
37
 
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';
83
- import '@auth-link/web/styles.css';
38
+ 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. A is minimal, B is split, C is compact.
84
39
 
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; });
40
+ ```tsx
41
+ import { AuthLinkPasscode } from '@auth-link/web/react';
42
+ // Keep client stable (for example with useMemo).
43
+ <AuthLinkPasscode client={client} expiresAt={pending.expiresAt}
44
+ locale="zh-CN" variant="B" onVerified={updateUI} />
45
+ ```
46
+ ```vue
47
+ <script setup>
48
+ import { AuthLinkPasscode } from '@auth-link/web/vue';
120
49
  </script>
121
-
122
50
  <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>
51
+ <AuthLinkPasscode :client="client" :expires-at="pending.expiresAt"
52
+ locale="zh-CN" variant="B" @verified="updateUI" />
132
53
  </template>
133
54
  ```
134
55
 
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.
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.
56
+ ## Errors and cleanup
277
57
 
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.
58
+ 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
59
 
280
- ## License
60
+ 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
61
 
282
- MIT. See LICENSE and THIRD_PARTY_NOTICES.md.
62
+ 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.