@auth-link/web 0.0.2 → 0.0.4

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.
@@ -0,0 +1,260 @@
1
+ # Go + Vue:从登录、绑定到确认发布
2
+
3
+ [English](./go-vue.en.md) | **简体中文** | [日本語](./go-vue.ja.md) · [SDK 参数](./README.zh-CN.md)
4
+
5
+ 本指南按实际 Go CMS + Vue 3 的接入顺序整理。下面使用可替换的示例域名,不依赖某台电脑的配置、测试账号、密码或租户密钥。网站负责登录、数据库和业务接口;Web SDK 负责展示和观察请求,Go SDK 负责后台调用与回执校验。
6
+
7
+ ## 1. 区分三个来源并安装 SDK
8
+
9
+ | 角色 | 示例来源 | 配置位置 |
10
+ | --- | --- | --- |
11
+ | Vue | `https://cms.example.com` | `apiOrigin` 指向下方的 Go 后台 |
12
+ | Go CMS API | `https://cms-api.example.com` | `baseUrl=https://cms-api.example.com/api/authlink` |
13
+ | AuthLink API | `https://api.getauthlink.com` | Go SDK 的 `Origin`;浏览器的 `authLinkOrigin` |
14
+
15
+ AuthLink API 使用生产地址 `https://api.getauthlink.com`;Vue 网站和 Go CMS 的域名仍是接入方自己的示例,不能替换成 AuthLink 官网域名。
16
+
17
+ 登记租户的站点 origin 是 Vue 网站来源;Go SDK 的 `Origin` 和凭据导出 JSON 中的 API origin 是 AuthLink API 来源。浏览器的 `baseUrl` 指向 Go 业务后台,不能指向 AuthLink 租户 API。公开运行配置只包含这三个 origin;`TenantID`、`APIKey` 保留在 Go 私密配置或环境变量中。
18
+
19
+ ```sh
20
+ npm install @auth-link/web@latest vue
21
+ # In your Go backend module, after unpacking the supplied Go SDK:
22
+ go mod edit -replace=authlink.local/sdk=./third_party/authlink-go
23
+ go mod edit -require=authlink.local/sdk@v0.0.0
24
+ go mod tidy
25
+ ```
26
+
27
+ Go SDK 当前随源码或官网的 Go SDK 源码包交付,保留 `third_party/authlink-go/go.mod`,导入 `authlink "authlink.local/sdk"`;这不是公共 `go get` 地址,也不是 npm 包内置的后台。用可信后台配置调用 `authlink.New(authlink.Config{Origin: authOrigin, TenantID: tenantID, APIKey: apiKey})`。
28
+
29
+ ## 2. 先完成网站登录,再读取会话
30
+
31
+ 本站现有登录接口(参考 CMS 为 `POST /api/login`)验证用户后签发 HttpOnly、Secure 的 session Cookie。Vue 用 `credentials:'include'` 调用 Go 的 `GET /api/session`。已登录时返回 `authenticated:true` 与 `csrf`;未登录时先回到网站登录流程。
32
+
33
+ `csrf` 是本 CMS 的响应字段名;SDK 配置项仍叫 `csrfToken`,写成 `csrfToken: () => session.value?.csrf ?? ''` 即可。SDK 不要求你的后台一定返回名为 `csrfToken` 的字段。令牌由 Go 为当前 session 生成并保存,浏览器只读取,不能用租户密钥或手机核对码代替。创建和结果查询均需验证 `X-CSRF-Token` 与同一 session 的值一致。
34
+
35
+ Vue 与 API 使用同一 HTTPS 主域名下的两个子域名时,是跨 origin、同 site。参考 CMS 的 host-only Cookie 不设置 `Domain`,使用 `SameSite=Strict; Secure; HttpOnly; Path=/`;请求目标仍为 API 域名。真正跨 site 时还需按浏览器 Cookie 策略配置,不能只依靠 CORS。
36
+
37
+ ## 3. Go 配置 CORS 与业务路由
38
+
39
+ ```http
40
+ Access-Control-Allow-Origin: https://cms.example.com
41
+ Access-Control-Allow-Credentials: true
42
+ Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS
43
+ Access-Control-Allow-Headers: Content-Type, X-CSRF-Token, Idempotency-Key
44
+ Vary: Origin
45
+ Cache-Control: no-store
46
+ ```
47
+
48
+ 只为精确允许的 Vue origin 返回这些 CORS 头;允许来源的正常和错误响应都要携带。`OPTIONS` 先验证来源、方法和请求头,成功返回 204,不要求登录或 CSRF;随后实际请求才验证登录、来源、CSRF 和归属。不能用 `*` 或无条件反射来源。上面包含 CMS 草稿编辑所用的 PUT;只接入 SDK 的站点可按实际路由收窄。
49
+
50
+ | 网站自己的路由 | 请求 | 返回 |
51
+ | --- | --- | --- |
52
+ | `GET /api/session` | — | `{authenticated:true, csrf:"…", subject:"…", username:"…"}`;未登录: `{authenticated:false}` |
53
+ | `POST /api/authlink/requests` | `{purpose:"bind"}` 或 `{purpose:"confirm",postId,version}`; `Idempotency-Key` | 公开 `Ticket` |
54
+ | `POST /api/authlink/requests/{id}/result` | `{}`;当前 session + CSRF | 待处理状态/displayCode 或已验证的 `confirmed` 回执 |
55
+ | `POST /api/posts/{id}/publish` | `{requestId,version}`; 当前 session + CSRF | `{ok:true}` 校验已保存的确认记录和业务意图后返回 |
56
+
57
+ 上述路由由 Go CMS 实现,不是安装 Web SDK 后自动出现的接口。`postId` / `version` / `/publish` 是本例业务约定,不是 AuthLink 平台字段。
58
+
59
+ ## 4. 创建前落库,消费后再返回 confirmed
60
+
61
+ ```text
62
+ Vue → Go /api/session → current session + csrf
63
+ Vue → Go /api/authlink/requests → Go SDK CreateRequest → public ticket
64
+ Vue dialog → Go /api/authlink/requests/{id}/result → Status + Consume
65
+ Go commits verified receipt → Vue confirmed event
66
+ Vue → Go /api/posts/{id}/publish → validate saved intent/receipt → publish
67
+ ```
68
+
69
+ Go 从可信 session 取稳定用户 ID,不能接受浏览器自报的 `externalUserId`。创建顺序:
70
+
71
+ 1. 校验 purpose、创建幂等键,以及文章归属和当前版本(若有)。
72
+ 2. 生成独立随机 state 和消费幂等键;把原创建键、CreateInput、当前用户/session 和业务意图落库,再向 AuthLink 发请求。网络结果不明时从数据库恢复同一记录、输入和键。
73
+ 3. 调用 `CreateRequest`,保存返回的 requestId、原始 expiresAt 与 ticket,去掉可选 qrSvg 后返回公开 ticket。对同一 session + 创建键加唯一约束,键复用到其他 purpose / 文章 / 版本时返回冲突。
74
+ 4. 结果接口按 requestId + 当前 session 加锁读取记录;已保存回执直接复用。用 `Status` 查询,只有 approved / consumed 才以保存的 `Expected` 和消费键调用 `Consume`。SDK 校验失败不能返回成功。
75
+ 5. 在事务里保存回执、用户/session 确认记录和审计,requestId / verificationId 建唯一约束,提交成功后才返回 `confirmed`。待处理状态保留原 displayCode,不跨数字期限缓存。
76
+
77
+ 下面是 SDK 调用层代码,**不是包含登录、数据库和路由的完整 HTTP 服务**;注释中的落库与事务必须由你的 Go 后台实现。初始化客户端、构造 Input、所有权检查及请求记录的读写按上述步骤完成。
78
+
79
+ ```go
80
+ package cmsauth
81
+
82
+ import (
83
+ "context"
84
+ authlink "authlink.local/sdk"
85
+ )
86
+
87
+ // Load this record from your database for the authenticated user/session.
88
+ // Save Input (including State), CreateKey and ConsumeKey BEFORE calling Create.
89
+ // Intent (post ID/version), ownership and unique constraints belong in your DB.
90
+ type StoredRequest struct {
91
+ Input authlink.CreateInput
92
+ CreateKey string
93
+ ConsumeKey string
94
+ Ticket authlink.Ticket
95
+ }
96
+
97
+ // On network retries, load the SAME record; do not regenerate State or keys.
98
+ func Create(ctx context.Context, sdk *authlink.Client, saved StoredRequest) (authlink.Ticket, error) {
99
+ return sdk.CreateRequest(ctx, saved.Input, saved.CreateKey)
100
+ }
101
+
102
+ type PublicConfirmation struct {
103
+ Status string `json:"status"`
104
+ RequestID string `json:"requestId"`
105
+ VerificationID string `json:"verificationId"`
106
+ Purpose string `json:"purpose"`
107
+ ExpiresAt string `json:"expiresAt"`
108
+ AuthenticationPolicy string `json:"authenticationPolicy"`
109
+ }
110
+
111
+ // Caller checks session/CSRF/ownership, locks the saved record and first returns
112
+ // any already-saved receipt. For a new confirmed result, caller must persist
113
+ // the receipt and confirmation in a DB transaction BEFORE returning HTTP 200.
114
+ func Poll(ctx context.Context, sdk *authlink.Client, saved StoredRequest) (any, error) {
115
+ status, err := sdk.Status(ctx, saved.Ticket.RequestID)
116
+ if err != nil { return nil, err }
117
+ if status.Status != "approved" && status.Status != "consumed" { return status, nil }
118
+ result, err := sdk.Consume(ctx, authlink.Expected{
119
+ RequestID: saved.Ticket.RequestID,
120
+ ExternalUserID: saved.Input.ExternalUserID,
121
+ Purpose: saved.Input.Purpose,
122
+ State: saved.Input.State,
123
+ ExpiresAt: saved.Ticket.ExpiresAt,
124
+ }, saved.ConsumeKey)
125
+ if err != nil { return nil, err }
126
+ return PublicConfirmation{
127
+ Status: "confirmed", RequestID: result.RequestID,
128
+ VerificationID: result.VerificationID, Purpose: result.Purpose,
129
+ ExpiresAt: saved.Ticket.ExpiresAt, AuthenticationPolicy: result.AuthenticationPolicy,
130
+ }, nil
131
+ }
132
+ ```
133
+
134
+ ## 5. Vue 组件:稳定 ticket、按业务绑定创建、清理与重试
135
+
136
+ 完整文件也包含在包内 [examples/vue-cms.vue](../examples/vue-cms.vue)。网站登录后挂载它,传入可信 `apiOrigin`、`authLinkOrigin`,可选传入后台加载的草稿 `post={id,version}`。父层用当前登录 session 的标识做组件 key,退出、重新登录或切换账号时先卸载旧组件并清空会话、旧 ticket、幂等键和待发布操作,再挂载新组件。不要只改用户名后继续使用旧 ticket。
137
+
138
+ ```vue
139
+ <script setup lang="ts">
140
+ import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue';
141
+ import { createAuthLinkClient, requestKey, validateTicket } from '@auth-link/web';
142
+ import type { Confirmation, Mode, Ticket } from '@auth-link/web';
143
+ import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/vue';
144
+ import '@auth-link/web/styles.css';
145
+
146
+ type Post = { id: string; version: number };
147
+ type Publication = Post & { requestId: string };
148
+ // Supply trusted deployment configuration. Mount after site login and remount
149
+ // with :key="session.subject" after login, logout or account/session changes.
150
+ const props = defineProps<{ apiOrigin: string; authLinkOrigin: string; post?: Post }>();
151
+ const session = ref<{ authenticated: boolean; csrf?: string } | null>(null);
152
+ const ticket = shallowRef<Ticket | null>(null);
153
+ const publication = shallowRef<Post | null>(null);
154
+ const retryPublish = shallowRef<Publication | null>(null);
155
+ const busy = ref(false), message = ref('');
156
+ let creation: { signature: string; key: string } | null = null;
157
+ const lifetime = new AbortController();
158
+ const client = markRaw(createAuthLinkClient({
159
+ baseUrl: `${props.apiOrigin}/api/authlink`,
160
+ allowedBackendOrigins: [props.apiOrigin],
161
+ credentials: 'include',
162
+ csrfToken: () => session.value?.csrf ?? '',
163
+ authLinkOrigin: props.authLinkOrigin,
164
+ }));
165
+
166
+ async function api(path: string, body?: unknown, headers: Record<string, string> = {}) {
167
+ const response = await fetch(`${props.apiOrigin}${path}`, {
168
+ method: body === undefined ? 'GET' : 'POST',
169
+ credentials: 'include', cache: 'no-store', redirect: 'error',
170
+ signal: lifetime.signal,
171
+ headers: body === undefined ? {} : {
172
+ 'Content-Type': 'application/json', 'X-CSRF-Token': session.value?.csrf ?? '', ...headers,
173
+ },
174
+ body: body === undefined ? undefined : JSON.stringify(body),
175
+ });
176
+ const value = await response.json();
177
+ if (response.status === 401) {
178
+ session.value = null; closeTicket(); retryPublish.value = null; creation = null;
179
+ }
180
+ if (!response.ok) throw new Error(value.code ?? 'REQUEST_FAILED');
181
+ return value;
182
+ }
183
+ function closeTicket() { ticket.value = null; publication.value = null; }
184
+ function showError(error: Error) { message.value = error.message; }
185
+ function finish(text: string) { closeTicket(); message.value = text; }
186
+
187
+ async function start(purpose: Mode, post?: Post) {
188
+ if (busy.value || ticket.value || !session.value?.authenticated) return;
189
+ busy.value = true; message.value = ''; retryPublish.value = null;
190
+ const intent = post ? { id: post.id, version: post.version } : null;
191
+ const signature = JSON.stringify({ purpose, postId: intent?.id, version: intent?.version });
192
+ try {
193
+ if (creation?.signature !== signature) creation = { signature, key: requestKey() };
194
+ // SDK create() sends only {purpose}. Business fields use your own API.
195
+ const value = intent
196
+ ? validateTicket(await api('/api/authlink/requests', {
197
+ purpose, postId: intent.id, version: intent.version,
198
+ }, { 'Idempotency-Key': creation.key }), purpose, props.authLinkOrigin)
199
+ : await client.create(purpose, { idempotencyKey: creation.key, signal: lifetime.signal });
200
+ if (lifetime.signal.aborted) return;
201
+ publication.value = intent; ticket.value = value; creation = null;
202
+ } catch (error) {
203
+ if (!lifetime.signal.aborted) showError(error as Error);
204
+ // Retain creation.key for a retry of the SAME purpose/post/version.
205
+ } finally { busy.value = false; }
206
+ }
207
+ async function publish() {
208
+ const intent = retryPublish.value;
209
+ if (!intent || busy.value) return;
210
+ busy.value = true;
211
+ try {
212
+ await api(`/api/posts/${intent.id}/publish`, { version: intent.version, requestId: intent.requestId });
213
+ retryPublish.value = null; message.value = 'Published. Reload your posts from the backend.';
214
+ } catch (error) { if (!lifetime.signal.aborted) showError(error as Error); }
215
+ finally { busy.value = false; }
216
+ }
217
+ async function confirmed(result: Confirmation) {
218
+ const intent = publication.value;
219
+ closeTicket();
220
+ if (intent) { retryPublish.value = { ...intent, requestId: result.requestId }; await publish(); }
221
+ else message.value = 'Verified. Reload account status from the backend.';
222
+ }
223
+ onMounted(async () => {
224
+ try {
225
+ const value = await api('/api/session');
226
+ if (value.authenticated !== true || typeof value.csrf !== 'string' || !value.csrf) {
227
+ throw new Error('LOGIN_REQUIRED');
228
+ }
229
+ session.value = value;
230
+ } catch (error) { if (!lifetime.signal.aborted) showError(error as Error); }
231
+ });
232
+ onUnmounted(() => { lifetime.abort(); closeTicket(); creation = null; retryPublish.value = null; });
233
+ </script>
234
+
235
+ <template>
236
+ <button :disabled="busy || !!ticket || !session?.authenticated" @click="start('bind')">Bind phone</button>
237
+ <button :disabled="busy || !!ticket || !session?.authenticated" @click="start('confirm')">Confirm</button>
238
+ <button v-if="post" :disabled="busy || !!ticket || !session?.authenticated" @click="start('confirm', post)">Confirm publication</button>
239
+ <button v-if="retryPublish" :disabled="busy || !!ticket" @click="publish">Retry the same publication</button>
240
+ <AuthLinkBind v-if="ticket?.purpose === 'bind'" :ticket="ticket" :client="client" variant="B"
241
+ @confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected')"
242
+ @expired="finish('Expired; start a new request')" @error="showError" />
243
+ <AuthLinkConfirm v-else-if="ticket?.purpose === 'confirm'" :ticket="ticket" :client="client" variant="B"
244
+ @confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected')"
245
+ @expired="finish('Expired; start a new request')" @error="showError" />
246
+ <p role="status">{{ message }}</p>
247
+ </template>
248
+ ```
249
+
250
+ 普通绑定/确认使用 `client.create(purpose, {idempotencyKey})`。该方法只发送 purpose,**不能传入 postId/version 扩展请求体**。确认发布通过自己的 fetch 发送这些业务字段,再用 `validateTicket(response, 'confirm', authLinkOrigin)` 校验;后续弹窗查询仍复用官方 client。
251
+
252
+ 同一次创建失败保留同一个键;用途或文章版本改变才换新键。`shallowRef` 保存 ticket,`markRaw` 保存客户端。组件挂载只打开和观察已有请求;不在 mounted/watch 中创建请求。关闭、拒绝和过期清空 ticket;网络错误保留弹窗,允许重试原请求。确认成功先清空 ticket,再调用独立业务接口;失败保留同一个 requestId / 文章 / 版本用于重试,先从后台确认真实状态后再刷新页面。上例省略网站自己的登录表单、文章列表和错误码中文映射。
253
+
254
+ ## 6. 发布接口必须再做后台检查
255
+
256
+ `confirmed` 回调只触发界面更新与下一次 API 请求,不是放行凭据。Go 的发布事务必须验证当前 session 对应的已落库回执,purpose=confirm、意图=publish、同一 postId、同一 version、文章归属和草稿状态,以及首次应用时尚未过期。成功时更新文章并标记请求 applied;重试已应用的同一操作返回原成功,不重复发布或写审计。请求不能用于另一文章或新版本,修改内容后须重新确认。前端不把文章内容、action 或 transactionHash 塞入 AuthLink 消费回执。
257
+
258
+ ## 验收顺序
259
+
260
+ 登录 → 读取 session → 查询后台真实账号状态(参考 CMS 通过 overview 调用 Go SDK `AccountStatus`)→ 未绑定时 bind → 手机扫码批准 → 后台消费并刷新账号状态 → 保存草稿 → 按 postId/version confirm → 手机批准 → 结果消费落库 → 独立发布接口 → 重复发布/错误版本/拒绝/过期验证。真实设备验收仍由用户在手机上完成;本地编译和模拟 HTTP 测试不能代替这一步。
@@ -0,0 +1,109 @@
1
+ <script setup lang="ts">
2
+ import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue';
3
+ import { createAuthLinkClient, requestKey, validateTicket } from '@auth-link/web';
4
+ import type { Confirmation, Mode, Ticket } from '@auth-link/web';
5
+ import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/vue';
6
+ import '@auth-link/web/styles.css';
7
+
8
+ type Post = { id: string; version: number };
9
+ type Publication = Post & { requestId: string };
10
+ // Supply trusted deployment configuration. Mount after site login and remount
11
+ // with :key="session.subject" after login, logout or account/session changes.
12
+ const props = defineProps<{ apiOrigin: string; authLinkOrigin: string; post?: Post }>();
13
+ const session = ref<{ authenticated: boolean; csrf?: string } | null>(null);
14
+ const ticket = shallowRef<Ticket | null>(null);
15
+ const publication = shallowRef<Post | null>(null);
16
+ const retryPublish = shallowRef<Publication | null>(null);
17
+ const busy = ref(false), message = ref('');
18
+ let creation: { signature: string; key: string } | null = null;
19
+ const lifetime = new AbortController();
20
+ const client = markRaw(createAuthLinkClient({
21
+ baseUrl: `${props.apiOrigin}/api/authlink`,
22
+ allowedBackendOrigins: [props.apiOrigin],
23
+ credentials: 'include',
24
+ csrfToken: () => session.value?.csrf ?? '',
25
+ authLinkOrigin: props.authLinkOrigin,
26
+ }));
27
+
28
+ async function api(path: string, body?: unknown, headers: Record<string, string> = {}) {
29
+ const response = await fetch(`${props.apiOrigin}${path}`, {
30
+ method: body === undefined ? 'GET' : 'POST',
31
+ credentials: 'include', cache: 'no-store', redirect: 'error',
32
+ signal: lifetime.signal,
33
+ headers: body === undefined ? {} : {
34
+ 'Content-Type': 'application/json', 'X-CSRF-Token': session.value?.csrf ?? '', ...headers,
35
+ },
36
+ body: body === undefined ? undefined : JSON.stringify(body),
37
+ });
38
+ const value = await response.json();
39
+ if (response.status === 401) {
40
+ session.value = null; closeTicket(); retryPublish.value = null; creation = null;
41
+ }
42
+ if (!response.ok) throw new Error(value.code ?? 'REQUEST_FAILED');
43
+ return value;
44
+ }
45
+ function closeTicket() { ticket.value = null; publication.value = null; }
46
+ function showError(error: Error) { message.value = error.message; }
47
+ function finish(text: string) { closeTicket(); message.value = text; }
48
+
49
+ async function start(purpose: Mode, post?: Post) {
50
+ if (busy.value || ticket.value || !session.value?.authenticated) return;
51
+ busy.value = true; message.value = ''; retryPublish.value = null;
52
+ const intent = post ? { id: post.id, version: post.version } : null;
53
+ const signature = JSON.stringify({ purpose, postId: intent?.id, version: intent?.version });
54
+ try {
55
+ if (creation?.signature !== signature) creation = { signature, key: requestKey() };
56
+ // SDK create() sends only {purpose}. Business fields use your own API.
57
+ const value = intent
58
+ ? validateTicket(await api('/api/authlink/requests', {
59
+ purpose, postId: intent.id, version: intent.version,
60
+ }, { 'Idempotency-Key': creation.key }), purpose, props.authLinkOrigin)
61
+ : await client.create(purpose, { idempotencyKey: creation.key, signal: lifetime.signal });
62
+ if (lifetime.signal.aborted) return;
63
+ publication.value = intent; ticket.value = value; creation = null;
64
+ } catch (error) {
65
+ if (!lifetime.signal.aborted) showError(error as Error);
66
+ // Retain creation.key for a retry of the SAME purpose/post/version.
67
+ } finally { busy.value = false; }
68
+ }
69
+ async function publish() {
70
+ const intent = retryPublish.value;
71
+ if (!intent || busy.value) return;
72
+ busy.value = true;
73
+ try {
74
+ await api(`/api/posts/${intent.id}/publish`, { version: intent.version, requestId: intent.requestId });
75
+ retryPublish.value = null; message.value = 'Published. Reload your posts from the backend.';
76
+ } catch (error) { if (!lifetime.signal.aborted) showError(error as Error); }
77
+ finally { busy.value = false; }
78
+ }
79
+ async function confirmed(result: Confirmation) {
80
+ const intent = publication.value;
81
+ closeTicket();
82
+ if (intent) { retryPublish.value = { ...intent, requestId: result.requestId }; await publish(); }
83
+ else message.value = 'Verified. Reload account status from the backend.';
84
+ }
85
+ onMounted(async () => {
86
+ try {
87
+ const value = await api('/api/session');
88
+ if (value.authenticated !== true || typeof value.csrf !== 'string' || !value.csrf) {
89
+ throw new Error('LOGIN_REQUIRED');
90
+ }
91
+ session.value = value;
92
+ } catch (error) { if (!lifetime.signal.aborted) showError(error as Error); }
93
+ });
94
+ onUnmounted(() => { lifetime.abort(); closeTicket(); creation = null; retryPublish.value = null; });
95
+ </script>
96
+
97
+ <template>
98
+ <button :disabled="busy || !!ticket || !session?.authenticated" @click="start('bind')">Bind phone</button>
99
+ <button :disabled="busy || !!ticket || !session?.authenticated" @click="start('confirm')">Confirm</button>
100
+ <button v-if="post" :disabled="busy || !!ticket || !session?.authenticated" @click="start('confirm', post)">Confirm publication</button>
101
+ <button v-if="retryPublish" :disabled="busy || !!ticket" @click="publish">Retry the same publication</button>
102
+ <AuthLinkBind v-if="ticket?.purpose === 'bind'" :ticket="ticket" :client="client" variant="B"
103
+ @confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected')"
104
+ @expired="finish('Expired; start a new request')" @error="showError" />
105
+ <AuthLinkConfirm v-else-if="ticket?.purpose === 'confirm'" :ticket="ticket" :client="client" variant="B"
106
+ @confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected')"
107
+ @expired="finish('Expired; start a new request')" @error="showError" />
108
+ <p role="status">{{ message }}</p>
109
+ </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@auth-link/web",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "private": false,
5
5
  "description": "Three responsive AuthLink dialog layouts for phone binding and mobile confirmation. Framework-neutral API, React and Vue, 13 languages, Simplified Chinese by default.",
6
6
  "type": "module",
@@ -11,7 +11,8 @@
11
11
  "docs",
12
12
  "CHANGELOG.md",
13
13
  "THIRD_PARTY_NOTICES.md",
14
- "LICENSE"
14
+ "LICENSE",
15
+ "examples/vue-cms.vue"
15
16
  ],
16
17
  "sideEffects": [
17
18
  "**/*.css"