zy-web-gate 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,18 +10,21 @@
10
10
 
11
11
  - ✅ 没输对密码看不到页面、输对能进、同主域跨子域不用重复输。
12
12
  - ✅ 真密码**只存在校验接口侧**,前端 bundle 里没有任何密码信息(连 hash 都没有)。
13
- - 不防 DevTools、不防伪造 cookie、不防直接扒静态资源 / API。
13
+ - 登录态 cookie 存的是后端签发的 **JWT**,每次进入调 `/check` 验签——能挡住「控制台伪造 cookie 直接进 UI」与「照搬旧 `=1` 绕过」。
14
+ - ❌ 不防 DevTools、不防绕过 UI 直接扒静态资源 / 调业务 API。
14
15
  - ❌ 不是账号体系、不是 OAuth、不是邮箱/短信验证码,就是「一个共享固定密码」。
15
16
 
16
17
  如果你需要真正的安全鉴权,请用后端鉴权 / 身份系统,本包不适用。
17
18
 
18
19
  ## 工作原理
19
20
 
20
- 1. 进入任一子站时,先查父域 cookie(`Domain=example.com`)。
21
- 2. cookie 有效 → 直接放行,不弹任何 UI。
22
- 3. cookie 无效 → 弹出密码输入页(原生 DOM,Shadow DOM 隔离样式)。
23
- 4. 用户输入密码 → `POST` 给校验接口 → 接口比对后只返回「对 / 错」。
24
- 5. 正确 写父域 cookie放行;之后所有同主域子站读到该 cookie 即免输。
21
+ 采用**两级接口**:先用一个固定 token 向「地址分发接口」换取真实校验地址,再去真实地址校验,避免真实接口地址被写死在 bundle 里。
22
+
23
+ 1. 进入任一子站时,先查父域 cookie(`Domain=example.com`,值为 JWT)。
24
+ 2. cookie 存在 换真实地址后调 `/check` 验签;**验过才放行**,不弹任何 UI。
25
+ 3. cookie 不存在 / 验签不过弹出密码输入页(原生 DOM,Shadow DOM 隔离样式)。
26
+ 4. 用户输入密码 → 换真实地址 → `POST {url}/verify` → 接口比对后返回「对 / 错」并在对时签发 JWT。
27
+ 5. 正确 → 把 JWT 写父域 cookie → 放行;之后所有同主域子站读到该 cookie 并验签通过即免输。
25
28
 
26
29
  **为什么用 cookie 而不是 localStorage**:`localStorage` 按 origin 隔离,子域之间读不到,无法实现「一次验证、全子域通行」。cookie 可通过 `Domain` 设置到父域被全部子域共享,这是纯前端实现跨子域登录态的唯一可靠机制。
27
30
 
@@ -93,22 +96,43 @@ ensureGate().then(() => {
93
96
  </script>
94
97
  ```
95
98
 
96
- ## 校验接口约定
99
+ ## 校验接口约定(两级 + JWT)
100
+
101
+ 接口分两级,地址分发接口写死在包内(`src/discover.js`),真实校验地址由分发接口动态返回。完整字段、示例与安全说明见 [INTEGRATION.md](./INTEGRATION.md)。
102
+
103
+ **第一级:地址分发**(换真实地址)
104
+
105
+ | | |
106
+ |---|---|
107
+ | 方法 | `GET` |
108
+ | URL | `src/discover.js` 的 `DISCOVER_URL`(写死在包内) |
109
+ | 必带请求头 | `token: <DISCOVER_TOKEN>`(非安全凭证,仅提高扒取门槛) |
110
+ | 成功 | HTTP 200,`{ "code": 200, "data": { "url": "真实校验地址" } }` |
97
111
 
98
- 校验接口地址**写死在包内**(`src/index.js` 的 `API_URL`),子站无需关心。改地址时改这个常量、发新版本,子站升级依赖即可。
112
+ > 改真实地址:改分发接口(mock)的返回值即可,无需发新版本;改分发接口本身的地址才需改 `src/discover.js` 发新版本。本包每次校验都重新分发拉取,地址变更即时生效。
99
113
 
100
- 本包不含密码,校验交给该接口完成:
114
+ **第二级:密码校验**
101
115
 
102
116
  | | |
103
117
  |---|---|
104
118
  | 方法 | `POST` |
119
+ | URL | `{真实地址}/verify` |
105
120
  | 请求体 | `{ "password": "用户输入的密码" }` |
106
- | 通过 | HTTP 200,`{ "code": 200, "data": { "match": true } }` |
121
+ | 通过 | HTTP 200,`{ "code": 0, "data": { "match": true, "token": "<JWT>" } }` |
107
122
  | 密码错 | 任何 `data.match !== true` 的响应(HTTP 401 也可) |
108
123
 
109
- 判定规则:**只有「HTTP ok 且 `data.match === true`」算通过**,其余一律当密码错。网络/接口异常会单独提示「网络异常」而非「密码错误」。
124
+ 判定规则:**只有「HTTP ok 且 `data.match === true`」算通过**,其余一律当密码错;通过时把 `data.token` 写入 cookie。网络/接口异常会单独提示「网络异常」而非「密码错误」。
125
+
126
+ **第三级:token 验签**(已有 cookie 时)
127
+
128
+ | | |
129
+ |---|---|
130
+ | 方法 | `POST` |
131
+ | URL | `{真实地址}/check` |
132
+ | token 位置 | 请求头 `Authorization: Bearer <token>`(同时也放进 body) |
133
+ | 有效 | HTTP 200,`{ "code": 0, "data": { "valid": true } }` |
110
134
 
111
- > 接口需放行 CORS(响应带 `Access-Control-Allow-Origin`,并正确处理 `OPTIONS` 预检),否则前端跨域调不通。本调用不带 cookie,`Allow-Origin: *` 即可。建议接口侧加限流防暴力猜密码。
135
+ > 接口需放行 CORS(响应带 `Access-Control-Allow-Origin`,并正确处理 `OPTIONS` 预检),否则前端跨域调不通。建议接口侧加限流防暴力猜密码。
112
136
 
113
137
  ## 配置项
114
138
 
@@ -116,8 +140,7 @@ ensureGate().then(() => {
116
140
 
117
141
  | 选项 | 默认 | 说明 |
118
142
  |---|---|---|
119
- | `cookieName` | `"zy_web_gate"` | 登录态 cookie |
120
- | `cookieValue` | `"1"` | 写入的 cookie 值 |
143
+ | `cookieName` | `"zy_web_gate"` | 登录态 cookie 名(值为后端签发的 JWT,不可配) |
121
144
  | `cookieDomain` | 自动推断 | 父域;不传则由当前 host 推断(如 `a.example.com` → `example.com`)。localhost / IP 自动不写 Domain |
122
145
  | `maxAgeDays` | `7` | 登录态有效天数 |
123
146
  | `sameSite` | `"Lax"` | cookie SameSite |
@@ -0,0 +1,20 @@
1
+ /**
2
+ * 向地址分发接口换取真实校验后端地址。
3
+ *
4
+ * 接口约定:
5
+ * GET DISCOVER_URL header: { token: DISCOVER_TOKEN }
6
+ * 成功 HTTP 200, { code:200, data:{ url:"真实校验地址" } }
7
+ * 失败 HTTP 401, { code:401, data:{} }
8
+ *
9
+ * @param {object} [opts]
10
+ * @param {number} [opts.timeoutMs=10000]
11
+ * @returns {Promise<string>} 真实校验后端地址(已去除末尾斜杠)
12
+ * @throws {Error} 网络异常、未授权或返回里没有 url 时抛出
13
+ */
14
+ export function discoverBaseUrl(opts?: {
15
+ timeoutMs?: number | undefined;
16
+ }): Promise<string>;
17
+ /** 地址分发接口 URL(写死在包内,改地址只改这里并发新版本)。 */
18
+ export const DISCOVER_URL: "https://m1.apifoxmock.com/m1/6205743-5899102-default/getWebRateAddress";
19
+ /** 换取真实地址必带的固定 token(非安全凭证,仅提高扒取门槛)。 */
20
+ export const DISCOVER_TOKEN: "qB8pgQh*pcbBRf3P";
package/dist/index.d.ts CHANGED
@@ -2,9 +2,10 @@
2
2
  * 访问门主入口。
3
3
  *
4
4
  * 行为:
5
- * 1. 若父域 cookie 已存在有效登录态 -> 直接 resolve(放行,无 UI)。
6
- * 2. 否则弹出密码页,等用户输入并通过接口校验。
7
- * 3. 校验通过 -> 写父域 cookie -> resolve。
5
+ * 1. 父域 cookie 已存在 JWT -> 换真实地址 -> 调 /check 验签;过则放行(无 UI)。
6
+ * 验签不过 / 网络异常 -> 视作未登录,进入弹框流程。
7
+ * 2. 弹出密码页,等用户输入并通过 /verify 校验。
8
+ * 3. 校验通过 -> 把后端签发的 JWT 写进父域 cookie -> resolve。
8
9
  *
9
10
  * 该 Promise 只在「已通过门禁」时 resolve;密码未通过时不会 resolve,
10
11
  * 因此把它放在 createApp().mount() 之前 await,可保证未通过时真实应用绝不挂载。
@@ -20,13 +21,9 @@ export function ensureGate(options?: GateOptions): Promise<void>;
20
21
  export function logoutGate(options?: GateOptions): void;
21
22
  export type GateOptions = {
22
23
  /**
23
- * 登录态 cookie 名,默认 "zy_web_gate"
24
+ * 登录态 cookie 名,默认 "zy_web_gate";值为后端签发的 JWT
24
25
  */
25
26
  cookieName?: string | undefined;
26
- /**
27
- * 写入的 cookie 值,默认 "1"
28
- */
29
- cookieValue?: string | undefined;
30
27
  /**
31
28
  * 显式指定父域;不传则自动推断(如 example.com)
32
29
  */
package/dist/verify.d.ts CHANGED
@@ -1,38 +1,63 @@
1
1
  /**
2
- * 校验结果。
2
+ * 密码校验结果。
3
3
  * @typedef {object} VerifyResult
4
- * @property {boolean} ok 密码是否正确
5
- * @property {boolean} [networkError] 是否因网络/接口异常导致(区别于「密码错」)
6
- * @property {string} [message] 可展示给用户的提示
4
+ * @property {boolean} ok 密码是否正确
5
+ * @property {string|null} [token] 后端签发的 JWT(ok 时有值),写入 cookie
6
+ * @property {boolean} [networkError] 是否因网络/接口异常导致(区别于「密码错」)
7
+ * @property {string} [message] 可展示给用户的提示
7
8
  */
8
9
  /**
9
- * 调用校验接口。
10
+ * 第二级:调 `{baseUrl}/verify` 校验密码。
10
11
  *
11
12
  * 接口约定:
12
- * 请求 POST apiUrl body: {"password": "用户输入"}
13
- * 通过 HTTP 200, {"code":200, "data":{"match":true}}
14
- * 密码错 HTTP 401, {"code":401, "data":{}}(或任何 match !== true 的响应)
13
+ * POST {baseUrl}/verify body: {"password":"用户输入"}
14
+ * 通过 HTTP 200, { code:0, data:{ match:true, token:"<JWT>", expiresIn:604800 } }
15
+ * 密码错 HTTP 401, { code:1, data:{ match:false } }
15
16
  *
16
- * 判定规则:只有「HTTP ok 且 data.match === true」才算密码正确;
17
- * 其余一律当密码错。网络层异常单独标记为 networkError,便于 UI 区分提示。
17
+ * 判定规则:只有「HTTP ok 且 data.match === true」才算通过;其余一律当密码错。
18
+ * 网络层异常单独标记 networkError,便于 UI 区分提示。
18
19
  *
19
- * @param {string} apiUrl
20
+ * @param {string} baseUrl 真实校验后端地址(不含末尾斜杠)
20
21
  * @param {string} password
21
22
  * @param {object} [opts]
22
23
  * @param {number} [opts.timeoutMs=10000]
23
24
  * @returns {Promise<VerifyResult>}
24
25
  */
25
- export function verifyPassword(apiUrl: string, password: string, opts?: {
26
+ export function verifyPassword(baseUrl: string, password: string, opts?: {
26
27
  timeoutMs?: number | undefined;
27
28
  }): Promise<VerifyResult>;
28
29
  /**
29
- * 校验结果。
30
+ * 第三级:调 `{baseUrl}/check` 验签 cookie 里的 JWT。
31
+ *
32
+ * 接口约定:
33
+ * POST {baseUrl}/check header: { Authorization: "Bearer <token>" }
34
+ * 有效 HTTP 200, { code:0, data:{ valid:true } }
35
+ * 无效/过期/伪造 HTTP 401, { code:1, data:{ valid:false } }
36
+ *
37
+ * 用于「已有 cookie」分支:验过才放行,否则重新弹密码框。
38
+ * 网络异常时返回 false(保守起见不放行),由调用方决定是否回退到弹框。
39
+ *
40
+ * @param {string} baseUrl 真实校验后端地址(不含末尾斜杠)
41
+ * @param {string} token cookie 里存的 JWT
42
+ * @param {object} [opts]
43
+ * @param {number} [opts.timeoutMs=10000]
44
+ * @returns {Promise<boolean>} token 是否有效
45
+ */
46
+ export function checkToken(baseUrl: string, token: string, opts?: {
47
+ timeoutMs?: number | undefined;
48
+ }): Promise<boolean>;
49
+ /**
50
+ * 密码校验结果。
30
51
  */
31
52
  export type VerifyResult = {
32
53
  /**
33
54
  * 密码是否正确
34
55
  */
35
56
  ok: boolean;
57
+ /**
58
+ * 后端签发的 JWT(ok 时有值),写入 cookie
59
+ */
60
+ token?: string | null | undefined;
36
61
  /**
37
62
  * 是否因网络/接口异常导致(区别于「密码错」)
38
63
  */
@@ -23,7 +23,7 @@ function r(e, t) {
23
23
  async function i(e, t, n = {}) {
24
24
  let { timeoutMs: r = 1e4 } = n, i = new AbortController(), a = setTimeout(() => i.abort(), r), o;
25
25
  try {
26
- o = await fetch(e, {
26
+ o = await fetch(`${e}/verify`, {
27
27
  method: "POST",
28
28
  headers: { "Content-Type": "application/json" },
29
29
  body: JSON.stringify({ password: t }),
@@ -43,56 +43,108 @@ async function i(e, t, n = {}) {
43
43
  } catch {
44
44
  s = null;
45
45
  }
46
- return o.ok && s && s.data && s.data.match === !0 ? { ok: !0 } : {
46
+ return o.ok && s && s.data && s.data.match === !0 ? {
47
+ ok: !0,
48
+ token: s.data && s.data.token || null
49
+ } : {
47
50
  ok: !1,
48
51
  networkError: !1,
49
52
  message: "密码错误"
50
53
  };
51
54
  }
55
+ async function a(e, t, n = {}) {
56
+ let { timeoutMs: r = 1e4 } = n, i = new AbortController(), a = setTimeout(() => i.abort(), r), o;
57
+ try {
58
+ o = await fetch(`${e}/check`, {
59
+ method: "POST",
60
+ headers: {
61
+ "Content-Type": "application/json",
62
+ Authorization: `Bearer ${t}`
63
+ },
64
+ body: JSON.stringify({ token: t }),
65
+ signal: i.signal
66
+ });
67
+ } catch {
68
+ return clearTimeout(a), !1;
69
+ }
70
+ clearTimeout(a);
71
+ let s = null;
72
+ try {
73
+ s = await o.json();
74
+ } catch {
75
+ s = null;
76
+ }
77
+ return !!(o.ok && s && s.data && s.data.valid === !0);
78
+ }
79
+ //#endregion
80
+ //#region src/discover.js
81
+ var o = "https://m1.apifoxmock.com/m1/6205743-5899102-default/getWebRateAddress", s = "qB8pgQh*pcbBRf3P";
82
+ async function c(e = {}) {
83
+ let { timeoutMs: t = 1e4 } = e, n = new AbortController(), r = setTimeout(() => n.abort(), t), i;
84
+ try {
85
+ i = await fetch(o, {
86
+ method: "GET",
87
+ headers: { token: s },
88
+ signal: n.signal
89
+ });
90
+ } catch {
91
+ throw Error("获取校验接口地址失败:网络异常");
92
+ } finally {
93
+ clearTimeout(r);
94
+ }
95
+ let a = null;
96
+ try {
97
+ a = await i.json();
98
+ } catch {
99
+ a = null;
100
+ }
101
+ let c = a && a.data && a.data.url;
102
+ if (!i.ok || a.code !== 200 || !c) throw Error("获取校验接口地址失败");
103
+ return String(c).replace(/\/+$/, "");
104
+ }
52
105
  //#endregion
53
106
  //#region src/ui.js
54
- var a = "\n:host { all: initial; }\n.mask {\n position: fixed; inset: 0; z-index: 2147483647;\n display: flex; align-items: center; justify-content: center;\n background: #0f1115;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n}\n.card {\n width: 320px; max-width: calc(100vw - 48px);\n padding: 32px 28px;\n background: #1b1e25; border-radius: 14px;\n box-shadow: 0 12px 40px rgba(0,0,0,.45);\n box-sizing: border-box;\n}\n.title { margin: 0 0 6px; font-size: 18px; font-weight: 600; color: #f2f4f8; }\n.subtitle { margin: 0 0 22px; font-size: 13px; color: #8a92a6; line-height: 1.5; }\n.field { position: relative; }\n.input {\n width: 100%; box-sizing: border-box;\n padding: 11px 13px; font-size: 14px;\n color: #f2f4f8; background: #11141a;\n border: 1px solid #2c313c; border-radius: 9px; outline: none;\n transition: border-color .15s;\n}\n.input:focus { border-color: #4c8dff; }\n.btn {\n width: 100%; margin-top: 14px; padding: 11px 0;\n font-size: 14px; font-weight: 600; color: #fff; cursor: pointer;\n background: #4c8dff; border: none; border-radius: 9px;\n transition: background .15s, opacity .15s;\n}\n.btn:hover { background: #3a7df0; }\n.btn:disabled { opacity: .6; cursor: not-allowed; }\n.error {\n min-height: 18px; margin-top: 12px;\n font-size: 12.5px; color: #ff6b6b; line-height: 1.4;\n}\n";
55
- function o(e) {
107
+ var l = "\n:host { all: initial; }\n.mask {\n position: fixed; inset: 0; z-index: 2147483647;\n display: flex; align-items: center; justify-content: center;\n background: #0f1115;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n}\n.card {\n width: 320px; max-width: calc(100vw - 48px);\n padding: 32px 28px;\n background: #1b1e25; border-radius: 14px;\n box-shadow: 0 12px 40px rgba(0,0,0,.45);\n box-sizing: border-box;\n}\n.title { margin: 0 0 6px; font-size: 18px; font-weight: 600; color: #f2f4f8; }\n.subtitle { margin: 0 0 22px; font-size: 13px; color: #8a92a6; line-height: 1.5; }\n.field { position: relative; }\n.input {\n width: 100%; box-sizing: border-box;\n padding: 11px 13px; font-size: 14px;\n color: #f2f4f8; background: #11141a;\n border: 1px solid #2c313c; border-radius: 9px; outline: none;\n transition: border-color .15s;\n}\n.input:focus { border-color: #4c8dff; }\n.btn {\n width: 100%; margin-top: 14px; padding: 11px 0;\n font-size: 14px; font-weight: 600; color: #fff; cursor: pointer;\n background: #4c8dff; border: none; border-radius: 9px;\n transition: background .15s, opacity .15s;\n}\n.btn:hover { background: #3a7df0; }\n.btn:disabled { opacity: .6; cursor: not-allowed; }\n.error {\n min-height: 18px; margin-top: 12px;\n font-size: 12.5px; color: #ff6b6b; line-height: 1.4;\n}\n";
108
+ function u(e) {
56
109
  let t = document.createElement("div");
57
110
  t.setAttribute("data-zy-web-gate", "");
58
111
  let n = t.attachShadow({ mode: "open" }), r = document.createElement("style");
59
- r.textContent = a, n.appendChild(r);
112
+ r.textContent = l, n.appendChild(r);
60
113
  let i = document.createElement("div");
61
114
  i.className = "mask", i.innerHTML = "\n <div class=\"card\">\n <h1 class=\"title\"></h1>\n <p class=\"subtitle\"></p>\n <div class=\"field\">\n <input class=\"input\" type=\"password\" autocomplete=\"current-password\" />\n </div>\n <button class=\"btn\" type=\"button\"></button>\n <div class=\"error\" aria-live=\"polite\"></div>\n </div>\n ", n.appendChild(i), n.querySelector(".title").textContent = e.title, n.querySelector(".subtitle").textContent = e.subtitle;
62
- let o = n.querySelector(".input"), s = n.querySelector(".btn"), c = n.querySelector(".error");
63
- o.placeholder = e.placeholder, s.textContent = e.buttonText;
64
- let l = !1;
115
+ let a = n.querySelector(".input"), o = n.querySelector(".btn"), s = n.querySelector(".error");
116
+ a.placeholder = e.placeholder, o.textContent = e.buttonText;
117
+ let c = !1;
65
118
  async function u() {
66
- if (l) return;
67
- let t = o.value;
119
+ if (c) return;
120
+ let t = a.value;
68
121
  if (!t) {
69
- c.textContent = "请输入密码";
122
+ s.textContent = "请输入密码";
70
123
  return;
71
124
  }
72
- l = !0, s.disabled = !0, c.textContent = "";
73
- let n = s.textContent;
74
- s.textContent = "验证中…";
125
+ c = !0, o.disabled = !0, s.textContent = "";
126
+ let n = o.textContent;
127
+ o.textContent = "验证中…";
75
128
  let r;
76
129
  try {
77
130
  r = await e.onSubmit(t);
78
131
  } finally {
79
- l = !1, s.disabled = !1, s.textContent = n;
132
+ c = !1, o.disabled = !1, o.textContent = n;
80
133
  }
81
- r && r.ok ? d() : (c.textContent = r && r.message || "密码错误", o.select());
134
+ r && r.ok ? d() : (s.textContent = r && r.message || "密码错误", a.select());
82
135
  }
83
- s.addEventListener("click", u), o.addEventListener("keydown", (e) => {
136
+ o.addEventListener("click", u), a.addEventListener("keydown", (e) => {
84
137
  e.key === "Enter" && u();
85
138
  });
86
139
  function d() {
87
140
  t.remove();
88
141
  }
89
- return document.body.appendChild(t), setTimeout(() => o.focus(), 0), { destroy: d };
142
+ return document.body.appendChild(t), setTimeout(() => a.focus(), 0), { destroy: d };
90
143
  }
91
144
  //#endregion
92
145
  //#region src/index.js
93
- var s = "https://m1.apifoxmock.com/m1/6205743-5899102-default/zy-check-password", c = {
146
+ var d = {
94
147
  cookieName: "zy_web_gate",
95
- cookieValue: "1",
96
148
  cookieDomain: void 0,
97
149
  maxAgeDays: 7,
98
150
  sameSite: "Lax",
@@ -103,41 +155,59 @@ var s = "https://m1.apifoxmock.com/m1/6205743-5899102-default/zy-check-password"
103
155
  buttonText: "进入",
104
156
  timeoutMs: 1e4
105
157
  };
106
- function l(t) {
158
+ function f(t) {
107
159
  let n = {
108
- ...c,
160
+ ...d,
109
161
  ...t || {}
110
162
  };
111
163
  return n.cookieDomain === void 0 && (n.cookieDomain = e()), n;
112
164
  }
113
- function u(e) {
114
- let r = l(e);
115
- return t(r.cookieName) === r.cookieValue ? Promise.resolve() : new Promise((e) => {
116
- o({
117
- title: r.title,
118
- subtitle: r.subtitle,
119
- placeholder: r.placeholder,
120
- buttonText: r.buttonText,
165
+ async function p(e) {
166
+ let o = f(e), s = t(o.cookieName);
167
+ if (s) {
168
+ try {
169
+ if (await a(await c({ timeoutMs: o.timeoutMs }), s, { timeoutMs: o.timeoutMs })) return;
170
+ } catch {}
171
+ r(o.cookieName, { domain: o.cookieDomain });
172
+ }
173
+ return new Promise((e) => {
174
+ u({
175
+ title: o.title,
176
+ subtitle: o.subtitle,
177
+ placeholder: o.placeholder,
178
+ buttonText: o.buttonText,
121
179
  onSubmit: async (t) => {
122
- let a = await i(s, t, { timeoutMs: r.timeoutMs });
123
- return a.ok && (n(r.cookieName, r.cookieValue, {
124
- domain: r.cookieDomain,
125
- maxAgeSeconds: r.maxAgeDays * 24 * 60 * 60,
126
- sameSite: r.sameSite,
127
- secure: r.secure
128
- }), e()), a;
180
+ let r;
181
+ try {
182
+ r = await c({ timeoutMs: o.timeoutMs });
183
+ } catch {
184
+ return {
185
+ ok: !1,
186
+ message: "网络异常,请稍后重试"
187
+ };
188
+ }
189
+ let a = await i(r, t, { timeoutMs: o.timeoutMs });
190
+ return a.ok && a.token ? (n(o.cookieName, a.token, {
191
+ domain: o.cookieDomain,
192
+ maxAgeSeconds: o.maxAgeDays * 24 * 60 * 60,
193
+ sameSite: o.sameSite,
194
+ secure: o.secure
195
+ }), e(), { ok: !0 }) : a.ok && !a.token ? {
196
+ ok: !1,
197
+ message: "登录态签发失败,请重试"
198
+ } : a;
129
199
  }
130
200
  });
131
201
  });
132
202
  }
133
- function d(t = {}) {
203
+ function m(t = {}) {
134
204
  let n = {
135
- ...c,
205
+ ...d,
136
206
  ...t
137
207
  }, i = n.cookieDomain === void 0 ? e() : n.cookieDomain;
138
208
  r(n.cookieName, { domain: i });
139
209
  }
140
210
  //#endregion
141
- export { u as ensureGate, e as inferParentDomain, d as logoutGate, t as readCookie };
211
+ export { p as ensureGate, e as inferParentDomain, m as logoutGate, t as readCookie };
142
212
 
143
213
  //# sourceMappingURL=zy-web-gate.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"zy-web-gate.js","names":[],"sources":["../src/cookie.js","../src/verify.js","../src/ui.js","../src/index.js"],"sourcesContent":["// 跨子域共享登录态的核心:父域 cookie 读写。\n//\n// 为什么必须用 cookie 而不是 localStorage:\n// localStorage 按 origin(scheme+host+port)隔离,a.example.com 写的东西\n// b.example.com 读不到,无法实现「一次验证、全子域通行」。\n// cookie 可以通过 Domain 属性设置到父域,被该域下所有子域共享读取。\n\n/**\n * 由当前 host 推断可用于跨子域共享的父域。\n *\n * 例如当前在 a.example.com,应返回 example.com,\n * 这样写入的 cookie 能被 my./b./x. 等所有兄弟子域读到。\n *\n * 注意:eu.org 在 Public Suffix List 上,浏览器禁止把 cookie 设到 eu.org,\n * 所以父域只能取到 example.com 这一层,刚好是我们要的、也是安全的。\n *\n * 这里采用「去掉最左一段」的朴素策略:a.example.com -> example.com。\n * 大多数「单层子域」场景够用;若有多级子域或想显式指定,用 options.cookieDomain 覆盖。\n *\n * @param {string} [hostname] 默认取 location.hostname\n * @returns {string} 用于 cookie Domain 的父域;localhost / IP 等场景返回空串(表示按当前 host)\n */\nexport function inferParentDomain(hostname) {\n const host = hostname || window.location.hostname;\n\n // localhost、单段主机名、IP 地址:不设 Domain,cookie 只对当前 host 生效。\n if (host === \"localhost\" || !host.includes(\".\")) return \"\";\n if (/^\\d{1,3}(\\.\\d{1,3}){3}$/.test(host)) return \"\";\n\n const parts = host.split(\".\");\n // 形如 example.com(两段):父域就是它自己。\n if (parts.length <= 2) return host;\n // 形如 a.example.com:去掉最左一段。\n return parts.slice(1).join(\".\");\n}\n\n/**\n * 读取指定名称的 cookie 值。\n * @param {string} name\n * @returns {string|null}\n */\nexport function readCookie(name) {\n const prefix = encodeURIComponent(name) + \"=\";\n const items = document.cookie ? document.cookie.split(\"; \") : [];\n for (const item of items) {\n if (item.indexOf(prefix) === 0) {\n return decodeURIComponent(item.slice(prefix.length));\n }\n }\n return null;\n}\n\n/**\n * 写入跨子域 cookie。\n *\n * @param {string} name\n * @param {string} value\n * @param {object} opts\n * @param {string} opts.domain cookie 的 Domain,传空串则不写 Domain(仅当前 host)\n * @param {number} opts.maxAgeSeconds 有效期(秒)\n * @param {string} [opts.sameSite=\"Lax\"]\n * @param {boolean} [opts.secure=true] HTTPS 下应为 true;本地 http 调试时会自动放宽\n * @param {string} [opts.path=\"/\"]\n */\nexport function writeCookie(name, value, opts) {\n const {\n domain,\n maxAgeSeconds,\n sameSite = \"Lax\",\n path = \"/\",\n } = opts;\n\n // 本地 http 调试时 Secure 会让 cookie 写不进去,这里按当前协议自动判断。\n const secure = opts.secure !== undefined\n ? opts.secure\n : window.location.protocol === \"https:\";\n\n let str =\n encodeURIComponent(name) + \"=\" + encodeURIComponent(value) +\n \"; path=\" + path +\n \"; max-age=\" + Math.floor(maxAgeSeconds) +\n \"; samesite=\" + sameSite;\n\n if (domain) str += \"; domain=\" + domain;\n if (secure) str += \"; secure\";\n\n document.cookie = str;\n}\n\n/**\n * 删除跨子域 cookie(用于「登出」)。Domain / Path 必须和写入时一致才能删掉。\n * @param {string} name\n * @param {object} opts\n * @param {string} opts.domain\n * @param {string} [opts.path=\"/\"]\n */\nexport function deleteCookie(name, opts) {\n const { domain, path = \"/\" } = opts;\n let str = encodeURIComponent(name) + \"=; path=\" + path + \"; max-age=0\";\n if (domain) str += \"; domain=\" + domain;\n document.cookie = str;\n}\n","// 把用户输入的密码发给校验接口,由接口判断对错。\n//\n// 设计要点(方案③):真密码只存在接口侧,前端 bundle 里没有任何密码信息。\n// 前端只发 { password },只收「对 / 错」。改密码只改接口,子站零改动。\n\n/**\n * 校验结果。\n * @typedef {object} VerifyResult\n * @property {boolean} ok 密码是否正确\n * @property {boolean} [networkError] 是否因网络/接口异常导致(区别于「密码错」)\n * @property {string} [message] 可展示给用户的提示\n */\n\n/**\n * 调用校验接口。\n *\n * 接口约定:\n * 请求 POST apiUrl body: {\"password\": \"用户输入\"}\n * 通过 HTTP 200, {\"code\":200, \"data\":{\"match\":true}}\n * 密码错 HTTP 401, {\"code\":401, \"data\":{}}(或任何 match !== true 的响应)\n *\n * 判定规则:只有「HTTP ok 且 data.match === true」才算密码正确;\n * 其余一律当密码错。网络层异常单独标记为 networkError,便于 UI 区分提示。\n *\n * @param {string} apiUrl\n * @param {string} password\n * @param {object} [opts]\n * @param {number} [opts.timeoutMs=10000]\n * @returns {Promise<VerifyResult>}\n */\nexport async function verifyPassword(apiUrl, password, opts = {}) {\n const { timeoutMs = 10000 } = opts;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let resp;\n try {\n resp = await fetch(apiUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ password }),\n signal: controller.signal,\n });\n } catch (err) {\n clearTimeout(timer);\n // fetch 抛错只可能是网络层问题(断网、CORS、超时 abort 等),不是密码错。\n return {\n ok: false,\n networkError: true,\n message: \"网络异常,请稍后重试\",\n };\n }\n clearTimeout(timer);\n\n let data = null;\n try {\n data = await resp.json();\n } catch {\n // 响应不是合法 JSON:当作密码错/异常处理,不放行。\n data = null;\n }\n\n const match = !!(resp.ok && data && data.data && data.data.match === true);\n\n if (match) return { ok: true };\n\n return {\n ok: false,\n networkError: false,\n message: \"密码错误\",\n };\n}\n","// 框架无关的密码输入页(原生 DOM)。\n//\n// 用 Shadow DOM 把样式和结构完全隔离,既不会污染子站的 CSS,\n// 也不会被子站的全局样式影响。任何前端项目(Vue / React / 纯 HTML)都能用。\n\nconst STYLE = `\n:host { all: initial; }\n.mask {\n position: fixed; inset: 0; z-index: 2147483647;\n display: flex; align-items: center; justify-content: center;\n background: #0f1115;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n}\n.card {\n width: 320px; max-width: calc(100vw - 48px);\n padding: 32px 28px;\n background: #1b1e25; border-radius: 14px;\n box-shadow: 0 12px 40px rgba(0,0,0,.45);\n box-sizing: border-box;\n}\n.title { margin: 0 0 6px; font-size: 18px; font-weight: 600; color: #f2f4f8; }\n.subtitle { margin: 0 0 22px; font-size: 13px; color: #8a92a6; line-height: 1.5; }\n.field { position: relative; }\n.input {\n width: 100%; box-sizing: border-box;\n padding: 11px 13px; font-size: 14px;\n color: #f2f4f8; background: #11141a;\n border: 1px solid #2c313c; border-radius: 9px; outline: none;\n transition: border-color .15s;\n}\n.input:focus { border-color: #4c8dff; }\n.btn {\n width: 100%; margin-top: 14px; padding: 11px 0;\n font-size: 14px; font-weight: 600; color: #fff; cursor: pointer;\n background: #4c8dff; border: none; border-radius: 9px;\n transition: background .15s, opacity .15s;\n}\n.btn:hover { background: #3a7df0; }\n.btn:disabled { opacity: .6; cursor: not-allowed; }\n.error {\n min-height: 18px; margin-top: 12px;\n font-size: 12.5px; color: #ff6b6b; line-height: 1.4;\n}\n`;\n\n/**\n * 创建并挂载密码页。返回一个对象,可用于销毁。\n *\n * @param {object} cfg\n * @param {string} cfg.title\n * @param {string} cfg.subtitle\n * @param {string} cfg.placeholder\n * @param {string} cfg.buttonText\n * @param {(password: string) => Promise<{ok: boolean, message?: string}>} cfg.onSubmit\n * 提交回调;返回 ok=true 时 UI 自动销毁,false 时显示 message。\n * @returns {{ destroy: () => void }}\n */\nexport function mountPasswordGate(cfg) {\n const host = document.createElement(\"div\");\n host.setAttribute(\"data-zy-web-gate\", \"\");\n const shadow = host.attachShadow({ mode: \"open\" });\n\n const style = document.createElement(\"style\");\n style.textContent = STYLE;\n shadow.appendChild(style);\n\n const mask = document.createElement(\"div\");\n mask.className = \"mask\";\n mask.innerHTML = `\n <div class=\"card\">\n <h1 class=\"title\"></h1>\n <p class=\"subtitle\"></p>\n <div class=\"field\">\n <input class=\"input\" type=\"password\" autocomplete=\"current-password\" />\n </div>\n <button class=\"btn\" type=\"button\"></button>\n <div class=\"error\" aria-live=\"polite\"></div>\n </div>\n `;\n shadow.appendChild(mask);\n\n // 用 textContent 赋值,避免把配置文本当 HTML 注入。\n shadow.querySelector(\".title\").textContent = cfg.title;\n shadow.querySelector(\".subtitle\").textContent = cfg.subtitle;\n const input = shadow.querySelector(\".input\");\n const btn = shadow.querySelector(\".btn\");\n const errorEl = shadow.querySelector(\".error\");\n input.placeholder = cfg.placeholder;\n btn.textContent = cfg.buttonText;\n\n let submitting = false;\n\n async function submit() {\n if (submitting) return;\n const password = input.value;\n if (!password) {\n errorEl.textContent = \"请输入密码\";\n return;\n }\n\n submitting = true;\n btn.disabled = true;\n errorEl.textContent = \"\";\n const originalBtnText = btn.textContent;\n btn.textContent = \"验证中…\";\n\n let result;\n try {\n result = await cfg.onSubmit(password);\n } finally {\n submitting = false;\n btn.disabled = false;\n btn.textContent = originalBtnText;\n }\n\n if (result && result.ok) {\n destroy();\n } else {\n errorEl.textContent = (result && result.message) || \"密码错误\";\n input.select();\n }\n }\n\n btn.addEventListener(\"click\", submit);\n input.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") submit();\n });\n\n function destroy() {\n host.remove();\n }\n\n document.body.appendChild(host);\n // 自动聚焦输入框,方便直接打字。\n setTimeout(() => input.focus(), 0);\n\n return { destroy };\n}\n","// zy-web-gate —— 纯前端「共享固定密码」页面访问门。\n//\n// 用法(在 Vue/React/任意前端入口、挂载真实应用之前调用):\n//\n// import { ensureGate } from \"zy-web-gate\";\n//\n// await ensureGate(); // 校验接口已固定在包内,零参数即可\n//\n// // 走到这里说明已通过门禁,再启动真实应用:\n// createApp(App).mount(\"#app\");\n//\n// 跨子域:验证通过后写父域 cookie(Domain=example.com),同主域其他子站\n// 读到该 cookie 即直接放行,无需再次输入密码。\n// 安全边界:纯前端页面遮挡,防君子不防小人;真密码只在校验接口侧。\n\nimport { inferParentDomain, readCookie, writeCookie, deleteCookie } from \"./cookie.js\";\nimport { verifyPassword } from \"./verify.js\";\nimport { mountPasswordGate } from \"./ui.js\";\n\n// 校验接口固定在包内(写死、不可传)。\n// 它不是秘密——本来就出现在前端网络请求里;写死可让子站接入零参数。\n// 改地址时只改这里、发新版本,子站升级依赖即可。\nconst API_URL = \"https://m1.apifoxmock.com/m1/6205743-5899102-default/zy-check-password\";\n\n/** @type {Required<GateOptions>} 默认配置 */\nconst DEFAULTS = {\n cookieName: \"zy_web_gate\",\n cookieValue: \"1\",\n cookieDomain: undefined, // undefined 表示自动推断父域\n maxAgeDays: 7,\n sameSite: \"Lax\",\n secure: undefined, // undefined 表示按当前协议自动判断\n title: \"访问验证\",\n subtitle: \"请输入访问密码后继续。\",\n placeholder: \"访问密码\",\n buttonText: \"进入\",\n timeoutMs: 10000,\n};\n\n/**\n * @typedef {object} GateOptions\n * @property {string} [cookieName] 登录态 cookie 名,默认 \"zy_web_gate\"\n * @property {string} [cookieValue] 写入的 cookie 值,默认 \"1\"\n * @property {string} [cookieDomain] 显式指定父域;不传则自动推断(如 example.com)\n * @property {number} [maxAgeDays] 登录态有效天数,默认 7\n * @property {string} [sameSite] cookie SameSite,默认 \"Lax\"\n * @property {boolean} [secure] cookie Secure;不传则按当前协议自动判断\n * @property {string} [title] 密码页标题\n * @property {string} [subtitle] 密码页副标题\n * @property {string} [placeholder] 输入框占位文案\n * @property {string} [buttonText] 按钮文案\n * @property {number} [timeoutMs] 接口超时毫秒,默认 10000\n */\n\n/**\n * 解析最终配置。\n * @param {GateOptions} [options]\n */\nfunction resolveConfig(options) {\n const cfg = { ...DEFAULTS, ...(options || {}) };\n if (cfg.cookieDomain === undefined) {\n cfg.cookieDomain = inferParentDomain();\n }\n return cfg;\n}\n\n/**\n * 访问门主入口。\n *\n * 行为:\n * 1. 若父域 cookie 已存在有效登录态 -> 直接 resolve(放行,无 UI)。\n * 2. 否则弹出密码页,等用户输入并通过接口校验。\n * 3. 校验通过 -> 写父域 cookie -> resolve。\n *\n * 该 Promise 只在「已通过门禁」时 resolve;密码未通过时不会 resolve,\n * 因此把它放在 createApp().mount() 之前 await,可保证未通过时真实应用绝不挂载。\n *\n * @param {GateOptions} [options]\n * @returns {Promise<void>}\n */\nexport function ensureGate(options) {\n const cfg = resolveConfig(options);\n\n // 已有登录态:直接放行。\n if (readCookie(cfg.cookieName) === cfg.cookieValue) {\n return Promise.resolve();\n }\n\n return new Promise((resolve) => {\n mountPasswordGate({\n title: cfg.title,\n subtitle: cfg.subtitle,\n placeholder: cfg.placeholder,\n buttonText: cfg.buttonText,\n onSubmit: async (password) => {\n const result = await verifyPassword(API_URL, password, {\n timeoutMs: cfg.timeoutMs,\n });\n if (result.ok) {\n writeCookie(cfg.cookieName, cfg.cookieValue, {\n domain: cfg.cookieDomain,\n maxAgeSeconds: cfg.maxAgeDays * 24 * 60 * 60,\n sameSite: cfg.sameSite,\n secure: cfg.secure,\n });\n // 通过门禁:UI 会在本回调返回 ok 后自动销毁,这里同时 resolve 外层 Promise,\n // 让 await ensureGate() 的调用方继续往下挂载真实应用。\n resolve();\n }\n return result;\n },\n });\n });\n}\n\n/**\n * 主动登出:清除父域 cookie。调用后下次进入任一子站会重新要求输入密码。\n * @param {GateOptions} options 至少需与 ensureGate 一致的 cookieName / cookieDomain\n */\nexport function logoutGate(options = {}) {\n const cfg = { ...DEFAULTS, ...options };\n const domain = cfg.cookieDomain === undefined\n ? inferParentDomain()\n : cfg.cookieDomain;\n deleteCookie(cfg.cookieName, { domain });\n}\n\nexport { inferParentDomain, readCookie } from \"./cookie.js\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"zy-web-gate.js","names":[],"sources":["../src/cookie.js","../src/verify.js","../src/discover.js","../src/ui.js","../src/index.js"],"sourcesContent":["// 跨子域共享登录态的核心:父域 cookie 读写。\n//\n// 为什么必须用 cookie 而不是 localStorage:\n// localStorage 按 origin(scheme+host+port)隔离,a.example.com 写的东西\n// b.example.com 读不到,无法实现「一次验证、全子域通行」。\n// cookie 可以通过 Domain 属性设置到父域,被该域下所有子域共享读取。\n\n/**\n * 由当前 host 推断可用于跨子域共享的父域。\n *\n * 例如当前在 a.example.com,应返回 example.com,\n * 这样写入的 cookie 能被 my./b./x. 等所有兄弟子域读到。\n *\n * 注意:eu.org 在 Public Suffix List 上,浏览器禁止把 cookie 设到 eu.org,\n * 所以父域只能取到 example.com 这一层,刚好是我们要的、也是安全的。\n *\n * 这里采用「去掉最左一段」的朴素策略:a.example.com -> example.com。\n * 大多数「单层子域」场景够用;若有多级子域或想显式指定,用 options.cookieDomain 覆盖。\n *\n * @param {string} [hostname] 默认取 location.hostname\n * @returns {string} 用于 cookie Domain 的父域;localhost / IP 等场景返回空串(表示按当前 host)\n */\nexport function inferParentDomain(hostname) {\n const host = hostname || window.location.hostname;\n\n // localhost、单段主机名、IP 地址:不设 Domain,cookie 只对当前 host 生效。\n if (host === \"localhost\" || !host.includes(\".\")) return \"\";\n if (/^\\d{1,3}(\\.\\d{1,3}){3}$/.test(host)) return \"\";\n\n const parts = host.split(\".\");\n // 形如 example.com(两段):父域就是它自己。\n if (parts.length <= 2) return host;\n // 形如 a.example.com:去掉最左一段。\n return parts.slice(1).join(\".\");\n}\n\n/**\n * 读取指定名称的 cookie 值。\n * @param {string} name\n * @returns {string|null}\n */\nexport function readCookie(name) {\n const prefix = encodeURIComponent(name) + \"=\";\n const items = document.cookie ? document.cookie.split(\"; \") : [];\n for (const item of items) {\n if (item.indexOf(prefix) === 0) {\n return decodeURIComponent(item.slice(prefix.length));\n }\n }\n return null;\n}\n\n/**\n * 写入跨子域 cookie。\n *\n * @param {string} name\n * @param {string} value\n * @param {object} opts\n * @param {string} opts.domain cookie 的 Domain,传空串则不写 Domain(仅当前 host)\n * @param {number} opts.maxAgeSeconds 有效期(秒)\n * @param {string} [opts.sameSite=\"Lax\"]\n * @param {boolean} [opts.secure=true] HTTPS 下应为 true;本地 http 调试时会自动放宽\n * @param {string} [opts.path=\"/\"]\n */\nexport function writeCookie(name, value, opts) {\n const {\n domain,\n maxAgeSeconds,\n sameSite = \"Lax\",\n path = \"/\",\n } = opts;\n\n // 本地 http 调试时 Secure 会让 cookie 写不进去,这里按当前协议自动判断。\n const secure = opts.secure !== undefined\n ? opts.secure\n : window.location.protocol === \"https:\";\n\n let str =\n encodeURIComponent(name) + \"=\" + encodeURIComponent(value) +\n \"; path=\" + path +\n \"; max-age=\" + Math.floor(maxAgeSeconds) +\n \"; samesite=\" + sameSite;\n\n if (domain) str += \"; domain=\" + domain;\n if (secure) str += \"; secure\";\n\n document.cookie = str;\n}\n\n/**\n * 删除跨子域 cookie(用于「登出」)。Domain / Path 必须和写入时一致才能删掉。\n * @param {string} name\n * @param {object} opts\n * @param {string} opts.domain\n * @param {string} [opts.path=\"/\"]\n */\nexport function deleteCookie(name, opts) {\n const { domain, path = \"/\" } = opts;\n let str = encodeURIComponent(name) + \"=; path=\" + path + \"; max-age=0\";\n if (domain) str += \"; domain=\" + domain;\n document.cookie = str;\n}\n","// 第二/三级:向真实校验后端(gate-auth-server)校验密码与 token。\n//\n// 设计要点:真密码只存在后端,前端 bundle 里没有任何密码信息。\n// 校验通过后端会签发一枚 JWT,前端把它存进 cookie;再次进入时调 /check\n// 验签——攻击者没有服务端密钥,签不出合法 token,伪造 cookie 进不来。\n\n/**\n * 密码校验结果。\n * @typedef {object} VerifyResult\n * @property {boolean} ok 密码是否正确\n * @property {string|null} [token] 后端签发的 JWT(ok 时有值),写入 cookie\n * @property {boolean} [networkError] 是否因网络/接口异常导致(区别于「密码错」)\n * @property {string} [message] 可展示给用户的提示\n */\n\n/**\n * 第二级:调 `{baseUrl}/verify` 校验密码。\n *\n * 接口约定:\n * POST {baseUrl}/verify body: {\"password\":\"用户输入\"}\n * 通过 HTTP 200, { code:0, data:{ match:true, token:\"<JWT>\", expiresIn:604800 } }\n * 密码错 HTTP 401, { code:1, data:{ match:false } }\n *\n * 判定规则:只有「HTTP ok 且 data.match === true」才算通过;其余一律当密码错。\n * 网络层异常单独标记 networkError,便于 UI 区分提示。\n *\n * @param {string} baseUrl 真实校验后端地址(不含末尾斜杠)\n * @param {string} password\n * @param {object} [opts]\n * @param {number} [opts.timeoutMs=10000]\n * @returns {Promise<VerifyResult>}\n */\nexport async function verifyPassword(baseUrl, password, opts = {}) {\n const { timeoutMs = 10000 } = opts;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let resp;\n try {\n resp = await fetch(`${baseUrl}/verify`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ password }),\n signal: controller.signal,\n });\n } catch {\n clearTimeout(timer);\n // fetch 抛错只可能是网络层问题(断网、CORS、超时 abort 等),不是密码错。\n return { ok: false, networkError: true, message: \"网络异常,请稍后重试\" };\n }\n clearTimeout(timer);\n\n let data = null;\n try {\n data = await resp.json();\n } catch {\n // 响应不是合法 JSON:当作密码错/异常处理,不放行。\n data = null;\n }\n\n const match = !!(resp.ok && data && data.data && data.data.match === true);\n\n if (match) {\n return { ok: true, token: (data.data && data.data.token) || null };\n }\n\n return { ok: false, networkError: false, message: \"密码错误\" };\n}\n\n/**\n * 第三级:调 `{baseUrl}/check` 验签 cookie 里的 JWT。\n *\n * 接口约定:\n * POST {baseUrl}/check header: { Authorization: \"Bearer <token>\" }\n * 有效 HTTP 200, { code:0, data:{ valid:true } }\n * 无效/过期/伪造 HTTP 401, { code:1, data:{ valid:false } }\n *\n * 用于「已有 cookie」分支:验过才放行,否则重新弹密码框。\n * 网络异常时返回 false(保守起见不放行),由调用方决定是否回退到弹框。\n *\n * @param {string} baseUrl 真实校验后端地址(不含末尾斜杠)\n * @param {string} token cookie 里存的 JWT\n * @param {object} [opts]\n * @param {number} [opts.timeoutMs=10000]\n * @returns {Promise<boolean>} token 是否有效\n */\nexport async function checkToken(baseUrl, token, opts = {}) {\n const { timeoutMs = 10000 } = opts;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let resp;\n try {\n resp = await fetch(`${baseUrl}/check`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({ token }),\n signal: controller.signal,\n });\n } catch {\n clearTimeout(timer);\n return false;\n }\n clearTimeout(timer);\n\n let data = null;\n try {\n data = await resp.json();\n } catch {\n data = null;\n }\n\n return !!(resp.ok && data && data.data && data.data.valid === true);\n}\n","// 第一级:地址分发接口。用一个固定 token 换取真实的校验后端地址。\n//\n// 为什么要这一级:避免真实校验接口地址被写死在前端 bundle 字符串里。\n// 这里的 token 不是安全凭证——它本来就会出现在前端请求里,作用只是\n// 「让真实地址不直接出现在 bundle 里」,提高一点点扒取门槛。\n// 真正的安全靠第二/三级的 JWT 校验。\n//\n// 设计选择:不在内存缓存 data.url,每次校验都重新分发拉取,\n// 这样后端在 mock 里改地址即时生效。\n\n/** 地址分发接口 URL(写死在包内,改地址只改这里并发新版本)。 */\nexport const DISCOVER_URL =\n \"https://m1.apifoxmock.com/m1/6205743-5899102-default/getWebRateAddress\";\n\n/** 换取真实地址必带的固定 token(非安全凭证,仅提高扒取门槛)。 */\nexport const DISCOVER_TOKEN = \"qB8pgQh*pcbBRf3P\";\n\n/**\n * 向地址分发接口换取真实校验后端地址。\n *\n * 接口约定:\n * GET DISCOVER_URL header: { token: DISCOVER_TOKEN }\n * 成功 HTTP 200, { code:200, data:{ url:\"真实校验地址\" } }\n * 失败 HTTP 401, { code:401, data:{} }\n *\n * @param {object} [opts]\n * @param {number} [opts.timeoutMs=10000]\n * @returns {Promise<string>} 真实校验后端地址(已去除末尾斜杠)\n * @throws {Error} 网络异常、未授权或返回里没有 url 时抛出\n */\nexport async function discoverBaseUrl(opts = {}) {\n const { timeoutMs = 10000 } = opts;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let resp;\n try {\n resp = await fetch(DISCOVER_URL, {\n method: \"GET\",\n headers: { token: DISCOVER_TOKEN },\n signal: controller.signal,\n });\n } catch {\n throw new Error(\"获取校验接口地址失败:网络异常\");\n } finally {\n clearTimeout(timer);\n }\n\n let data = null;\n try {\n data = await resp.json();\n } catch {\n data = null;\n }\n\n const url = data && data.data && data.data.url;\n if (!resp.ok || data.code !== 200 || !url) {\n throw new Error(\"获取校验接口地址失败\");\n }\n\n // 去掉末尾斜杠,方便后续拼 `${base}/verify`、`${base}/check`。\n return String(url).replace(/\\/+$/, \"\");\n}\n","// 框架无关的密码输入页(原生 DOM)。\n//\n// 用 Shadow DOM 把样式和结构完全隔离,既不会污染子站的 CSS,\n// 也不会被子站的全局样式影响。任何前端项目(Vue / React / 纯 HTML)都能用。\n\nconst STYLE = `\n:host { all: initial; }\n.mask {\n position: fixed; inset: 0; z-index: 2147483647;\n display: flex; align-items: center; justify-content: center;\n background: #0f1115;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n}\n.card {\n width: 320px; max-width: calc(100vw - 48px);\n padding: 32px 28px;\n background: #1b1e25; border-radius: 14px;\n box-shadow: 0 12px 40px rgba(0,0,0,.45);\n box-sizing: border-box;\n}\n.title { margin: 0 0 6px; font-size: 18px; font-weight: 600; color: #f2f4f8; }\n.subtitle { margin: 0 0 22px; font-size: 13px; color: #8a92a6; line-height: 1.5; }\n.field { position: relative; }\n.input {\n width: 100%; box-sizing: border-box;\n padding: 11px 13px; font-size: 14px;\n color: #f2f4f8; background: #11141a;\n border: 1px solid #2c313c; border-radius: 9px; outline: none;\n transition: border-color .15s;\n}\n.input:focus { border-color: #4c8dff; }\n.btn {\n width: 100%; margin-top: 14px; padding: 11px 0;\n font-size: 14px; font-weight: 600; color: #fff; cursor: pointer;\n background: #4c8dff; border: none; border-radius: 9px;\n transition: background .15s, opacity .15s;\n}\n.btn:hover { background: #3a7df0; }\n.btn:disabled { opacity: .6; cursor: not-allowed; }\n.error {\n min-height: 18px; margin-top: 12px;\n font-size: 12.5px; color: #ff6b6b; line-height: 1.4;\n}\n`;\n\n/**\n * 创建并挂载密码页。返回一个对象,可用于销毁。\n *\n * @param {object} cfg\n * @param {string} cfg.title\n * @param {string} cfg.subtitle\n * @param {string} cfg.placeholder\n * @param {string} cfg.buttonText\n * @param {(password: string) => Promise<{ok: boolean, message?: string}>} cfg.onSubmit\n * 提交回调;返回 ok=true 时 UI 自动销毁,false 时显示 message。\n * @returns {{ destroy: () => void }}\n */\nexport function mountPasswordGate(cfg) {\n const host = document.createElement(\"div\");\n host.setAttribute(\"data-zy-web-gate\", \"\");\n const shadow = host.attachShadow({ mode: \"open\" });\n\n const style = document.createElement(\"style\");\n style.textContent = STYLE;\n shadow.appendChild(style);\n\n const mask = document.createElement(\"div\");\n mask.className = \"mask\";\n mask.innerHTML = `\n <div class=\"card\">\n <h1 class=\"title\"></h1>\n <p class=\"subtitle\"></p>\n <div class=\"field\">\n <input class=\"input\" type=\"password\" autocomplete=\"current-password\" />\n </div>\n <button class=\"btn\" type=\"button\"></button>\n <div class=\"error\" aria-live=\"polite\"></div>\n </div>\n `;\n shadow.appendChild(mask);\n\n // 用 textContent 赋值,避免把配置文本当 HTML 注入。\n shadow.querySelector(\".title\").textContent = cfg.title;\n shadow.querySelector(\".subtitle\").textContent = cfg.subtitle;\n const input = shadow.querySelector(\".input\");\n const btn = shadow.querySelector(\".btn\");\n const errorEl = shadow.querySelector(\".error\");\n input.placeholder = cfg.placeholder;\n btn.textContent = cfg.buttonText;\n\n let submitting = false;\n\n async function submit() {\n if (submitting) return;\n const password = input.value;\n if (!password) {\n errorEl.textContent = \"请输入密码\";\n return;\n }\n\n submitting = true;\n btn.disabled = true;\n errorEl.textContent = \"\";\n const originalBtnText = btn.textContent;\n btn.textContent = \"验证中…\";\n\n let result;\n try {\n result = await cfg.onSubmit(password);\n } finally {\n submitting = false;\n btn.disabled = false;\n btn.textContent = originalBtnText;\n }\n\n if (result && result.ok) {\n destroy();\n } else {\n errorEl.textContent = (result && result.message) || \"密码错误\";\n input.select();\n }\n }\n\n btn.addEventListener(\"click\", submit);\n input.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") submit();\n });\n\n function destroy() {\n host.remove();\n }\n\n document.body.appendChild(host);\n // 自动聚焦输入框,方便直接打字。\n setTimeout(() => input.focus(), 0);\n\n return { destroy };\n}\n","// zy-web-gate —— 纯前端「共享密码」页面访问门(两级接口 + JWT 方案)。\n//\n// 用法(在 Vue/React/任意前端入口、挂载真实应用之前调用):\n//\n// import { ensureGate } from \"zy-web-gate\";\n//\n// await ensureGate(); // 接口已固定在包内,零参数即可\n//\n// // 走到这里说明已通过门禁,再启动真实应用:\n// createApp(App).mount(\"#app\");\n//\n// 流程(详见 INTEGRATION.md):\n// 1. 已有 cookie(JWT)?-> 调 /check 验签,过则直接放行(无 UI)。\n// 2. 否则:GET 地址分发接口换真实校验地址 -> 弹密码框 -> POST /verify。\n// 3. 通过 -> 把后端签发的 JWT 写进父域 cookie -> 放行。\n//\n// 跨子域:验证通过后写父域 cookie(Domain=example.com),同主域其他子站\n// 读到该 cookie 并验签通过即直接放行,无需再次输入密码。\n//\n// 安全边界:纯前端门禁防的是「伪造 cookie 进 UI」与「照搬旧 =1 绕过」;\n// 挡不住绕过 UI 直接扒静态资源 / 直接打业务 API——真正的安全必须靠\n// 业务数据接口自己校验 token(即第三级 /check)。务必全链路走 HTTPS。\n\nimport { inferParentDomain, readCookie, writeCookie, deleteCookie } from \"./cookie.js\";\nimport { verifyPassword, checkToken } from \"./verify.js\";\nimport { discoverBaseUrl } from \"./discover.js\";\nimport { mountPasswordGate } from \"./ui.js\";\n\n/** @type {Required<GateOptions>} 默认配置 */\nconst DEFAULTS = {\n cookieName: \"zy_web_gate\",\n cookieDomain: undefined, // undefined 表示自动推断父域\n maxAgeDays: 7,\n sameSite: \"Lax\",\n secure: undefined, // undefined 表示按当前协议自动判断\n title: \"访问验证\",\n subtitle: \"请输入访问密码后继续。\",\n placeholder: \"访问密码\",\n buttonText: \"进入\",\n timeoutMs: 10000,\n};\n\n/**\n * @typedef {object} GateOptions\n * @property {string} [cookieName] 登录态 cookie 名,默认 \"zy_web_gate\";值为后端签发的 JWT\n * @property {string} [cookieDomain] 显式指定父域;不传则自动推断(如 example.com)\n * @property {number} [maxAgeDays] 登录态有效天数,默认 7\n * @property {string} [sameSite] cookie SameSite,默认 \"Lax\"\n * @property {boolean} [secure] cookie Secure;不传则按当前协议自动判断\n * @property {string} [title] 密码页标题\n * @property {string} [subtitle] 密码页副标题\n * @property {string} [placeholder] 输入框占位文案\n * @property {string} [buttonText] 按钮文案\n * @property {number} [timeoutMs] 接口超时毫秒,默认 10000\n */\n\n/**\n * 解析最终配置。\n * @param {GateOptions} [options]\n */\nfunction resolveConfig(options) {\n const cfg = { ...DEFAULTS, ...(options || {}) };\n if (cfg.cookieDomain === undefined) {\n cfg.cookieDomain = inferParentDomain();\n }\n return cfg;\n}\n\n/**\n * 访问门主入口。\n *\n * 行为:\n * 1. 父域 cookie 已存在 JWT -> 换真实地址 -> 调 /check 验签;过则放行(无 UI)。\n * 验签不过 / 网络异常 -> 视作未登录,进入弹框流程。\n * 2. 弹出密码页,等用户输入并通过 /verify 校验。\n * 3. 校验通过 -> 把后端签发的 JWT 写进父域 cookie -> resolve。\n *\n * 该 Promise 只在「已通过门禁」时 resolve;密码未通过时不会 resolve,\n * 因此把它放在 createApp().mount() 之前 await,可保证未通过时真实应用绝不挂载。\n *\n * @param {GateOptions} [options]\n * @returns {Promise<void>}\n */\nexport async function ensureGate(options) {\n const cfg = resolveConfig(options);\n\n // 已有 cookie:取出 JWT,换真实地址后调 /check 验签——验过才放行。\n // 这一步是「JWT 方案」相对旧 =1 方案的关键:仅 cookie 存在不够,必须验签,\n // 否则攻击者随便塞个字符串就能进 UI。\n const existing = readCookie(cfg.cookieName);\n if (existing) {\n try {\n const baseUrl = await discoverBaseUrl({ timeoutMs: cfg.timeoutMs });\n if (await checkToken(baseUrl, existing, { timeoutMs: cfg.timeoutMs })) {\n return; // 验签通过,放行。\n }\n } catch {\n // 换地址失败:保守起见不放行,落到下方弹框流程。\n }\n // 验签未过:清掉无效 cookie,避免每次都白跑一遍 /check。\n deleteCookie(cfg.cookieName, { domain: cfg.cookieDomain });\n }\n\n return new Promise((resolve) => {\n mountPasswordGate({\n title: cfg.title,\n subtitle: cfg.subtitle,\n placeholder: cfg.placeholder,\n buttonText: cfg.buttonText,\n onSubmit: async (password) => {\n let baseUrl;\n try {\n baseUrl = await discoverBaseUrl({ timeoutMs: cfg.timeoutMs });\n } catch {\n return { ok: false, message: \"网络异常,请稍后重试\" };\n }\n\n const result = await verifyPassword(baseUrl, password, {\n timeoutMs: cfg.timeoutMs,\n });\n\n if (result.ok && result.token) {\n writeCookie(cfg.cookieName, result.token, {\n domain: cfg.cookieDomain,\n maxAgeSeconds: cfg.maxAgeDays * 24 * 60 * 60,\n sameSite: cfg.sameSite,\n secure: cfg.secure,\n });\n // 通过门禁:UI 会在本回调返回 ok 后自动销毁,这里同时 resolve 外层 Promise,\n // 让 await ensureGate() 的调用方继续往下挂载真实应用。\n resolve();\n return { ok: true };\n }\n\n // 密码对但后端没给 token:当作异常,不放行(避免写空 cookie)。\n if (result.ok && !result.token) {\n return { ok: false, message: \"登录态签发失败,请重试\" };\n }\n\n return result;\n },\n });\n });\n}\n\n/**\n * 主动登出:清除父域 cookie。调用后下次进入任一子站会重新要求输入密码。\n * @param {GateOptions} options 至少需与 ensureGate 一致的 cookieName / cookieDomain\n */\nexport function logoutGate(options = {}) {\n const cfg = { ...DEFAULTS, ...options };\n const domain = cfg.cookieDomain === undefined\n ? inferParentDomain()\n : cfg.cookieDomain;\n deleteCookie(cfg.cookieName, { domain });\n}\n\nexport { inferParentDomain, readCookie } from \"./cookie.js\";\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.ZyWebGate={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(e){let t=e||window.location.hostname;if(t===`localhost`||!t.includes(`.`)||/^\d{1,3}(\.\d{1,3}){3}$/.test(t))return``;let n=t.split(`.`);return n.length<=2?t:n.slice(1).join(`.`)}function n(e){let t=encodeURIComponent(e)+`=`,n=document.cookie?document.cookie.split(`; `):[];for(let e of n)if(e.indexOf(t)===0)return decodeURIComponent(e.slice(t.length));return null}function r(e,t,n){let{domain:r,maxAgeSeconds:i,sameSite:a=`Lax`,path:o=`/`}=n,s=n.secure===void 0?window.location.protocol===`https:`:n.secure,c=encodeURIComponent(e)+`=`+encodeURIComponent(t)+`; path=`+o+`; max-age=`+Math.floor(i)+`; samesite=`+a;r&&(c+=`; domain=`+r),s&&(c+=`; secure`),document.cookie=c}function i(e,t){let{domain:n,path:r=`/`}=t,i=encodeURIComponent(e)+`=; path=`+r+`; max-age=0`;n&&(i+=`; domain=`+n),document.cookie=i}async function a(e,t,n={}){let{timeoutMs:r=1e4}=n,i=new AbortController,a=setTimeout(()=>i.abort(),r),o;try{o=await fetch(e,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({password:t}),signal:i.signal})}catch{return clearTimeout(a),{ok:!1,networkError:!0,message:`网络异常,请稍后重试`}}clearTimeout(a);let s=null;try{s=await o.json()}catch{s=null}return o.ok&&s&&s.data&&s.data.match===!0?{ok:!0}:{ok:!1,networkError:!1,message:`密码错误`}}var o=`
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.ZyWebGate={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(e){let t=e||window.location.hostname;if(t===`localhost`||!t.includes(`.`)||/^\d{1,3}(\.\d{1,3}){3}$/.test(t))return``;let n=t.split(`.`);return n.length<=2?t:n.slice(1).join(`.`)}function n(e){let t=encodeURIComponent(e)+`=`,n=document.cookie?document.cookie.split(`; `):[];for(let e of n)if(e.indexOf(t)===0)return decodeURIComponent(e.slice(t.length));return null}function r(e,t,n){let{domain:r,maxAgeSeconds:i,sameSite:a=`Lax`,path:o=`/`}=n,s=n.secure===void 0?window.location.protocol===`https:`:n.secure,c=encodeURIComponent(e)+`=`+encodeURIComponent(t)+`; path=`+o+`; max-age=`+Math.floor(i)+`; samesite=`+a;r&&(c+=`; domain=`+r),s&&(c+=`; secure`),document.cookie=c}function i(e,t){let{domain:n,path:r=`/`}=t,i=encodeURIComponent(e)+`=; path=`+r+`; max-age=0`;n&&(i+=`; domain=`+n),document.cookie=i}async function a(e,t,n={}){let{timeoutMs:r=1e4}=n,i=new AbortController,a=setTimeout(()=>i.abort(),r),o;try{o=await fetch(`${e}/verify`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({password:t}),signal:i.signal})}catch{return clearTimeout(a),{ok:!1,networkError:!0,message:`网络异常,请稍后重试`}}clearTimeout(a);let s=null;try{s=await o.json()}catch{s=null}return o.ok&&s&&s.data&&s.data.match===!0?{ok:!0,token:s.data&&s.data.token||null}:{ok:!1,networkError:!1,message:`密码错误`}}async function o(e,t,n={}){let{timeoutMs:r=1e4}=n,i=new AbortController,a=setTimeout(()=>i.abort(),r),o;try{o=await fetch(`${e}/check`,{method:`POST`,headers:{"Content-Type":`application/json`,Authorization:`Bearer ${t}`},body:JSON.stringify({token:t}),signal:i.signal})}catch{return clearTimeout(a),!1}clearTimeout(a);let s=null;try{s=await o.json()}catch{s=null}return!!(o.ok&&s&&s.data&&s.data.valid===!0)}var s=`https://m1.apifoxmock.com/m1/6205743-5899102-default/getWebRateAddress`,c=`qB8pgQh*pcbBRf3P`;async function l(e={}){let{timeoutMs:t=1e4}=e,n=new AbortController,r=setTimeout(()=>n.abort(),t),i;try{i=await fetch(s,{method:`GET`,headers:{token:c},signal:n.signal})}catch{throw Error(`获取校验接口地址失败:网络异常`)}finally{clearTimeout(r)}let a=null;try{a=await i.json()}catch{a=null}let o=a&&a.data&&a.data.url;if(!i.ok||a.code!==200||!o)throw Error(`获取校验接口地址失败`);return String(o).replace(/\/+$/,``)}var u=`
2
2
  :host { all: initial; }
3
3
  .mask {
4
4
  position: fixed; inset: 0; z-index: 2147483647;
@@ -36,7 +36,7 @@
36
36
  min-height: 18px; margin-top: 12px;
37
37
  font-size: 12.5px; color: #ff6b6b; line-height: 1.4;
38
38
  }
39
- `;function s(e){let t=document.createElement(`div`);t.setAttribute(`data-zy-web-gate`,``);let n=t.attachShadow({mode:`open`}),r=document.createElement(`style`);r.textContent=o,n.appendChild(r);let i=document.createElement(`div`);i.className=`mask`,i.innerHTML=`
39
+ `;function d(e){let t=document.createElement(`div`);t.setAttribute(`data-zy-web-gate`,``);let n=t.attachShadow({mode:`open`}),r=document.createElement(`style`);r.textContent=u,n.appendChild(r);let i=document.createElement(`div`);i.className=`mask`,i.innerHTML=`
40
40
  <div class="card">
41
41
  <h1 class="title"></h1>
42
42
  <p class="subtitle"></p>
@@ -46,5 +46,5 @@
46
46
  <button class="btn" type="button"></button>
47
47
  <div class="error" aria-live="polite"></div>
48
48
  </div>
49
- `,n.appendChild(i),n.querySelector(`.title`).textContent=e.title,n.querySelector(`.subtitle`).textContent=e.subtitle;let a=n.querySelector(`.input`),s=n.querySelector(`.btn`),c=n.querySelector(`.error`);a.placeholder=e.placeholder,s.textContent=e.buttonText;let l=!1;async function u(){if(l)return;let t=a.value;if(!t){c.textContent=`请输入密码`;return}l=!0,s.disabled=!0,c.textContent=``;let n=s.textContent;s.textContent=`验证中…`;let r;try{r=await e.onSubmit(t)}finally{l=!1,s.disabled=!1,s.textContent=n}r&&r.ok?d():(c.textContent=r&&r.message||`密码错误`,a.select())}s.addEventListener(`click`,u),a.addEventListener(`keydown`,e=>{e.key===`Enter`&&u()});function d(){t.remove()}return document.body.appendChild(t),setTimeout(()=>a.focus(),0),{destroy:d}}var c=`https://m1.apifoxmock.com/m1/6205743-5899102-default/zy-check-password`,l={cookieName:`zy_web_gate`,cookieValue:`1`,cookieDomain:void 0,maxAgeDays:7,sameSite:`Lax`,secure:void 0,title:`访问验证`,subtitle:`请输入访问密码后继续。`,placeholder:`访问密码`,buttonText:`进入`,timeoutMs:1e4};function u(e){let n={...l,...e||{}};return n.cookieDomain===void 0&&(n.cookieDomain=t()),n}function d(e){let t=u(e);return n(t.cookieName)===t.cookieValue?Promise.resolve():new Promise(e=>{s({title:t.title,subtitle:t.subtitle,placeholder:t.placeholder,buttonText:t.buttonText,onSubmit:async n=>{let i=await a(c,n,{timeoutMs:t.timeoutMs});return i.ok&&(r(t.cookieName,t.cookieValue,{domain:t.cookieDomain,maxAgeSeconds:t.maxAgeDays*24*60*60,sameSite:t.sameSite,secure:t.secure}),e()),i}})})}function f(e={}){let n={...l,...e},r=n.cookieDomain===void 0?t():n.cookieDomain;i(n.cookieName,{domain:r})}e.ensureGate=d,e.inferParentDomain=t,e.logoutGate=f,e.readCookie=n});
49
+ `,n.appendChild(i),n.querySelector(`.title`).textContent=e.title,n.querySelector(`.subtitle`).textContent=e.subtitle;let a=n.querySelector(`.input`),o=n.querySelector(`.btn`),s=n.querySelector(`.error`);a.placeholder=e.placeholder,o.textContent=e.buttonText;let c=!1;async function l(){if(c)return;let t=a.value;if(!t){s.textContent=`请输入密码`;return}c=!0,o.disabled=!0,s.textContent=``;let n=o.textContent;o.textContent=`验证中…`;let r;try{r=await e.onSubmit(t)}finally{c=!1,o.disabled=!1,o.textContent=n}r&&r.ok?d():(s.textContent=r&&r.message||`密码错误`,a.select())}o.addEventListener(`click`,l),a.addEventListener(`keydown`,e=>{e.key===`Enter`&&l()});function d(){t.remove()}return document.body.appendChild(t),setTimeout(()=>a.focus(),0),{destroy:d}}var f={cookieName:`zy_web_gate`,cookieDomain:void 0,maxAgeDays:7,sameSite:`Lax`,secure:void 0,title:`访问验证`,subtitle:`请输入访问密码后继续。`,placeholder:`访问密码`,buttonText:`进入`,timeoutMs:1e4};function p(e){let n={...f,...e||{}};return n.cookieDomain===void 0&&(n.cookieDomain=t()),n}async function m(e){let t=p(e),s=n(t.cookieName);if(s){try{if(await o(await l({timeoutMs:t.timeoutMs}),s,{timeoutMs:t.timeoutMs}))return}catch{}i(t.cookieName,{domain:t.cookieDomain})}return new Promise(e=>{d({title:t.title,subtitle:t.subtitle,placeholder:t.placeholder,buttonText:t.buttonText,onSubmit:async n=>{let i;try{i=await l({timeoutMs:t.timeoutMs})}catch{return{ok:!1,message:`网络异常,请稍后重试`}}let o=await a(i,n,{timeoutMs:t.timeoutMs});return o.ok&&o.token?(r(t.cookieName,o.token,{domain:t.cookieDomain,maxAgeSeconds:t.maxAgeDays*24*60*60,sameSite:t.sameSite,secure:t.secure}),e(),{ok:!0}):o.ok&&!o.token?{ok:!1,message:`登录态签发失败,请重试`}:o}})})}function h(e={}){let n={...f,...e},r=n.cookieDomain===void 0?t():n.cookieDomain;i(n.cookieName,{domain:r})}e.ensureGate=m,e.inferParentDomain=t,e.logoutGate=h,e.readCookie=n});
50
50
  //# sourceMappingURL=zy-web-gate.umd.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"zy-web-gate.umd.cjs","names":[],"sources":["../src/cookie.js","../src/verify.js","../src/ui.js","../src/index.js"],"sourcesContent":["// 跨子域共享登录态的核心:父域 cookie 读写。\n//\n// 为什么必须用 cookie 而不是 localStorage:\n// localStorage 按 origin(scheme+host+port)隔离,a.example.com 写的东西\n// b.example.com 读不到,无法实现「一次验证、全子域通行」。\n// cookie 可以通过 Domain 属性设置到父域,被该域下所有子域共享读取。\n\n/**\n * 由当前 host 推断可用于跨子域共享的父域。\n *\n * 例如当前在 a.example.com,应返回 example.com,\n * 这样写入的 cookie 能被 my./b./x. 等所有兄弟子域读到。\n *\n * 注意:eu.org 在 Public Suffix List 上,浏览器禁止把 cookie 设到 eu.org,\n * 所以父域只能取到 example.com 这一层,刚好是我们要的、也是安全的。\n *\n * 这里采用「去掉最左一段」的朴素策略:a.example.com -> example.com。\n * 大多数「单层子域」场景够用;若有多级子域或想显式指定,用 options.cookieDomain 覆盖。\n *\n * @param {string} [hostname] 默认取 location.hostname\n * @returns {string} 用于 cookie Domain 的父域;localhost / IP 等场景返回空串(表示按当前 host)\n */\nexport function inferParentDomain(hostname) {\n const host = hostname || window.location.hostname;\n\n // localhost、单段主机名、IP 地址:不设 Domain,cookie 只对当前 host 生效。\n if (host === \"localhost\" || !host.includes(\".\")) return \"\";\n if (/^\\d{1,3}(\\.\\d{1,3}){3}$/.test(host)) return \"\";\n\n const parts = host.split(\".\");\n // 形如 example.com(两段):父域就是它自己。\n if (parts.length <= 2) return host;\n // 形如 a.example.com:去掉最左一段。\n return parts.slice(1).join(\".\");\n}\n\n/**\n * 读取指定名称的 cookie 值。\n * @param {string} name\n * @returns {string|null}\n */\nexport function readCookie(name) {\n const prefix = encodeURIComponent(name) + \"=\";\n const items = document.cookie ? document.cookie.split(\"; \") : [];\n for (const item of items) {\n if (item.indexOf(prefix) === 0) {\n return decodeURIComponent(item.slice(prefix.length));\n }\n }\n return null;\n}\n\n/**\n * 写入跨子域 cookie。\n *\n * @param {string} name\n * @param {string} value\n * @param {object} opts\n * @param {string} opts.domain cookie 的 Domain,传空串则不写 Domain(仅当前 host)\n * @param {number} opts.maxAgeSeconds 有效期(秒)\n * @param {string} [opts.sameSite=\"Lax\"]\n * @param {boolean} [opts.secure=true] HTTPS 下应为 true;本地 http 调试时会自动放宽\n * @param {string} [opts.path=\"/\"]\n */\nexport function writeCookie(name, value, opts) {\n const {\n domain,\n maxAgeSeconds,\n sameSite = \"Lax\",\n path = \"/\",\n } = opts;\n\n // 本地 http 调试时 Secure 会让 cookie 写不进去,这里按当前协议自动判断。\n const secure = opts.secure !== undefined\n ? opts.secure\n : window.location.protocol === \"https:\";\n\n let str =\n encodeURIComponent(name) + \"=\" + encodeURIComponent(value) +\n \"; path=\" + path +\n \"; max-age=\" + Math.floor(maxAgeSeconds) +\n \"; samesite=\" + sameSite;\n\n if (domain) str += \"; domain=\" + domain;\n if (secure) str += \"; secure\";\n\n document.cookie = str;\n}\n\n/**\n * 删除跨子域 cookie(用于「登出」)。Domain / Path 必须和写入时一致才能删掉。\n * @param {string} name\n * @param {object} opts\n * @param {string} opts.domain\n * @param {string} [opts.path=\"/\"]\n */\nexport function deleteCookie(name, opts) {\n const { domain, path = \"/\" } = opts;\n let str = encodeURIComponent(name) + \"=; path=\" + path + \"; max-age=0\";\n if (domain) str += \"; domain=\" + domain;\n document.cookie = str;\n}\n","// 把用户输入的密码发给校验接口,由接口判断对错。\n//\n// 设计要点(方案③):真密码只存在接口侧,前端 bundle 里没有任何密码信息。\n// 前端只发 { password },只收「对 / 错」。改密码只改接口,子站零改动。\n\n/**\n * 校验结果。\n * @typedef {object} VerifyResult\n * @property {boolean} ok 密码是否正确\n * @property {boolean} [networkError] 是否因网络/接口异常导致(区别于「密码错」)\n * @property {string} [message] 可展示给用户的提示\n */\n\n/**\n * 调用校验接口。\n *\n * 接口约定:\n * 请求 POST apiUrl body: {\"password\": \"用户输入\"}\n * 通过 HTTP 200, {\"code\":200, \"data\":{\"match\":true}}\n * 密码错 HTTP 401, {\"code\":401, \"data\":{}}(或任何 match !== true 的响应)\n *\n * 判定规则:只有「HTTP ok 且 data.match === true」才算密码正确;\n * 其余一律当密码错。网络层异常单独标记为 networkError,便于 UI 区分提示。\n *\n * @param {string} apiUrl\n * @param {string} password\n * @param {object} [opts]\n * @param {number} [opts.timeoutMs=10000]\n * @returns {Promise<VerifyResult>}\n */\nexport async function verifyPassword(apiUrl, password, opts = {}) {\n const { timeoutMs = 10000 } = opts;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let resp;\n try {\n resp = await fetch(apiUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ password }),\n signal: controller.signal,\n });\n } catch (err) {\n clearTimeout(timer);\n // fetch 抛错只可能是网络层问题(断网、CORS、超时 abort 等),不是密码错。\n return {\n ok: false,\n networkError: true,\n message: \"网络异常,请稍后重试\",\n };\n }\n clearTimeout(timer);\n\n let data = null;\n try {\n data = await resp.json();\n } catch {\n // 响应不是合法 JSON:当作密码错/异常处理,不放行。\n data = null;\n }\n\n const match = !!(resp.ok && data && data.data && data.data.match === true);\n\n if (match) return { ok: true };\n\n return {\n ok: false,\n networkError: false,\n message: \"密码错误\",\n };\n}\n","// 框架无关的密码输入页(原生 DOM)。\n//\n// 用 Shadow DOM 把样式和结构完全隔离,既不会污染子站的 CSS,\n// 也不会被子站的全局样式影响。任何前端项目(Vue / React / 纯 HTML)都能用。\n\nconst STYLE = `\n:host { all: initial; }\n.mask {\n position: fixed; inset: 0; z-index: 2147483647;\n display: flex; align-items: center; justify-content: center;\n background: #0f1115;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n}\n.card {\n width: 320px; max-width: calc(100vw - 48px);\n padding: 32px 28px;\n background: #1b1e25; border-radius: 14px;\n box-shadow: 0 12px 40px rgba(0,0,0,.45);\n box-sizing: border-box;\n}\n.title { margin: 0 0 6px; font-size: 18px; font-weight: 600; color: #f2f4f8; }\n.subtitle { margin: 0 0 22px; font-size: 13px; color: #8a92a6; line-height: 1.5; }\n.field { position: relative; }\n.input {\n width: 100%; box-sizing: border-box;\n padding: 11px 13px; font-size: 14px;\n color: #f2f4f8; background: #11141a;\n border: 1px solid #2c313c; border-radius: 9px; outline: none;\n transition: border-color .15s;\n}\n.input:focus { border-color: #4c8dff; }\n.btn {\n width: 100%; margin-top: 14px; padding: 11px 0;\n font-size: 14px; font-weight: 600; color: #fff; cursor: pointer;\n background: #4c8dff; border: none; border-radius: 9px;\n transition: background .15s, opacity .15s;\n}\n.btn:hover { background: #3a7df0; }\n.btn:disabled { opacity: .6; cursor: not-allowed; }\n.error {\n min-height: 18px; margin-top: 12px;\n font-size: 12.5px; color: #ff6b6b; line-height: 1.4;\n}\n`;\n\n/**\n * 创建并挂载密码页。返回一个对象,可用于销毁。\n *\n * @param {object} cfg\n * @param {string} cfg.title\n * @param {string} cfg.subtitle\n * @param {string} cfg.placeholder\n * @param {string} cfg.buttonText\n * @param {(password: string) => Promise<{ok: boolean, message?: string}>} cfg.onSubmit\n * 提交回调;返回 ok=true 时 UI 自动销毁,false 时显示 message。\n * @returns {{ destroy: () => void }}\n */\nexport function mountPasswordGate(cfg) {\n const host = document.createElement(\"div\");\n host.setAttribute(\"data-zy-web-gate\", \"\");\n const shadow = host.attachShadow({ mode: \"open\" });\n\n const style = document.createElement(\"style\");\n style.textContent = STYLE;\n shadow.appendChild(style);\n\n const mask = document.createElement(\"div\");\n mask.className = \"mask\";\n mask.innerHTML = `\n <div class=\"card\">\n <h1 class=\"title\"></h1>\n <p class=\"subtitle\"></p>\n <div class=\"field\">\n <input class=\"input\" type=\"password\" autocomplete=\"current-password\" />\n </div>\n <button class=\"btn\" type=\"button\"></button>\n <div class=\"error\" aria-live=\"polite\"></div>\n </div>\n `;\n shadow.appendChild(mask);\n\n // 用 textContent 赋值,避免把配置文本当 HTML 注入。\n shadow.querySelector(\".title\").textContent = cfg.title;\n shadow.querySelector(\".subtitle\").textContent = cfg.subtitle;\n const input = shadow.querySelector(\".input\");\n const btn = shadow.querySelector(\".btn\");\n const errorEl = shadow.querySelector(\".error\");\n input.placeholder = cfg.placeholder;\n btn.textContent = cfg.buttonText;\n\n let submitting = false;\n\n async function submit() {\n if (submitting) return;\n const password = input.value;\n if (!password) {\n errorEl.textContent = \"请输入密码\";\n return;\n }\n\n submitting = true;\n btn.disabled = true;\n errorEl.textContent = \"\";\n const originalBtnText = btn.textContent;\n btn.textContent = \"验证中…\";\n\n let result;\n try {\n result = await cfg.onSubmit(password);\n } finally {\n submitting = false;\n btn.disabled = false;\n btn.textContent = originalBtnText;\n }\n\n if (result && result.ok) {\n destroy();\n } else {\n errorEl.textContent = (result && result.message) || \"密码错误\";\n input.select();\n }\n }\n\n btn.addEventListener(\"click\", submit);\n input.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") submit();\n });\n\n function destroy() {\n host.remove();\n }\n\n document.body.appendChild(host);\n // 自动聚焦输入框,方便直接打字。\n setTimeout(() => input.focus(), 0);\n\n return { destroy };\n}\n","// zy-web-gate —— 纯前端「共享固定密码」页面访问门。\n//\n// 用法(在 Vue/React/任意前端入口、挂载真实应用之前调用):\n//\n// import { ensureGate } from \"zy-web-gate\";\n//\n// await ensureGate(); // 校验接口已固定在包内,零参数即可\n//\n// // 走到这里说明已通过门禁,再启动真实应用:\n// createApp(App).mount(\"#app\");\n//\n// 跨子域:验证通过后写父域 cookie(Domain=example.com),同主域其他子站\n// 读到该 cookie 即直接放行,无需再次输入密码。\n// 安全边界:纯前端页面遮挡,防君子不防小人;真密码只在校验接口侧。\n\nimport { inferParentDomain, readCookie, writeCookie, deleteCookie } from \"./cookie.js\";\nimport { verifyPassword } from \"./verify.js\";\nimport { mountPasswordGate } from \"./ui.js\";\n\n// 校验接口固定在包内(写死、不可传)。\n// 它不是秘密——本来就出现在前端网络请求里;写死可让子站接入零参数。\n// 改地址时只改这里、发新版本,子站升级依赖即可。\nconst API_URL = \"https://m1.apifoxmock.com/m1/6205743-5899102-default/zy-check-password\";\n\n/** @type {Required<GateOptions>} 默认配置 */\nconst DEFAULTS = {\n cookieName: \"zy_web_gate\",\n cookieValue: \"1\",\n cookieDomain: undefined, // undefined 表示自动推断父域\n maxAgeDays: 7,\n sameSite: \"Lax\",\n secure: undefined, // undefined 表示按当前协议自动判断\n title: \"访问验证\",\n subtitle: \"请输入访问密码后继续。\",\n placeholder: \"访问密码\",\n buttonText: \"进入\",\n timeoutMs: 10000,\n};\n\n/**\n * @typedef {object} GateOptions\n * @property {string} [cookieName] 登录态 cookie 名,默认 \"zy_web_gate\"\n * @property {string} [cookieValue] 写入的 cookie 值,默认 \"1\"\n * @property {string} [cookieDomain] 显式指定父域;不传则自动推断(如 example.com)\n * @property {number} [maxAgeDays] 登录态有效天数,默认 7\n * @property {string} [sameSite] cookie SameSite,默认 \"Lax\"\n * @property {boolean} [secure] cookie Secure;不传则按当前协议自动判断\n * @property {string} [title] 密码页标题\n * @property {string} [subtitle] 密码页副标题\n * @property {string} [placeholder] 输入框占位文案\n * @property {string} [buttonText] 按钮文案\n * @property {number} [timeoutMs] 接口超时毫秒,默认 10000\n */\n\n/**\n * 解析最终配置。\n * @param {GateOptions} [options]\n */\nfunction resolveConfig(options) {\n const cfg = { ...DEFAULTS, ...(options || {}) };\n if (cfg.cookieDomain === undefined) {\n cfg.cookieDomain = inferParentDomain();\n }\n return cfg;\n}\n\n/**\n * 访问门主入口。\n *\n * 行为:\n * 1. 若父域 cookie 已存在有效登录态 -> 直接 resolve(放行,无 UI)。\n * 2. 否则弹出密码页,等用户输入并通过接口校验。\n * 3. 校验通过 -> 写父域 cookie -> resolve。\n *\n * 该 Promise 只在「已通过门禁」时 resolve;密码未通过时不会 resolve,\n * 因此把它放在 createApp().mount() 之前 await,可保证未通过时真实应用绝不挂载。\n *\n * @param {GateOptions} [options]\n * @returns {Promise<void>}\n */\nexport function ensureGate(options) {\n const cfg = resolveConfig(options);\n\n // 已有登录态:直接放行。\n if (readCookie(cfg.cookieName) === cfg.cookieValue) {\n return Promise.resolve();\n }\n\n return new Promise((resolve) => {\n mountPasswordGate({\n title: cfg.title,\n subtitle: cfg.subtitle,\n placeholder: cfg.placeholder,\n buttonText: cfg.buttonText,\n onSubmit: async (password) => {\n const result = await verifyPassword(API_URL, password, {\n timeoutMs: cfg.timeoutMs,\n });\n if (result.ok) {\n writeCookie(cfg.cookieName, cfg.cookieValue, {\n domain: cfg.cookieDomain,\n maxAgeSeconds: cfg.maxAgeDays * 24 * 60 * 60,\n sameSite: cfg.sameSite,\n secure: cfg.secure,\n });\n // 通过门禁:UI 会在本回调返回 ok 后自动销毁,这里同时 resolve 外层 Promise,\n // 让 await ensureGate() 的调用方继续往下挂载真实应用。\n resolve();\n }\n return result;\n },\n });\n });\n}\n\n/**\n * 主动登出:清除父域 cookie。调用后下次进入任一子站会重新要求输入密码。\n * @param {GateOptions} options 至少需与 ensureGate 一致的 cookieName / cookieDomain\n */\nexport function logoutGate(options = {}) {\n const cfg = { ...DEFAULTS, ...options };\n const domain = cfg.cookieDomain === undefined\n ? inferParentDomain()\n : cfg.cookieDomain;\n deleteCookie(cfg.cookieName, { domain });\n}\n\nexport { inferParentDomain, readCookie } from \"./cookie.js\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"zy-web-gate.umd.cjs","names":[],"sources":["../src/cookie.js","../src/verify.js","../src/discover.js","../src/ui.js","../src/index.js"],"sourcesContent":["// 跨子域共享登录态的核心:父域 cookie 读写。\n//\n// 为什么必须用 cookie 而不是 localStorage:\n// localStorage 按 origin(scheme+host+port)隔离,a.example.com 写的东西\n// b.example.com 读不到,无法实现「一次验证、全子域通行」。\n// cookie 可以通过 Domain 属性设置到父域,被该域下所有子域共享读取。\n\n/**\n * 由当前 host 推断可用于跨子域共享的父域。\n *\n * 例如当前在 a.example.com,应返回 example.com,\n * 这样写入的 cookie 能被 my./b./x. 等所有兄弟子域读到。\n *\n * 注意:eu.org 在 Public Suffix List 上,浏览器禁止把 cookie 设到 eu.org,\n * 所以父域只能取到 example.com 这一层,刚好是我们要的、也是安全的。\n *\n * 这里采用「去掉最左一段」的朴素策略:a.example.com -> example.com。\n * 大多数「单层子域」场景够用;若有多级子域或想显式指定,用 options.cookieDomain 覆盖。\n *\n * @param {string} [hostname] 默认取 location.hostname\n * @returns {string} 用于 cookie Domain 的父域;localhost / IP 等场景返回空串(表示按当前 host)\n */\nexport function inferParentDomain(hostname) {\n const host = hostname || window.location.hostname;\n\n // localhost、单段主机名、IP 地址:不设 Domain,cookie 只对当前 host 生效。\n if (host === \"localhost\" || !host.includes(\".\")) return \"\";\n if (/^\\d{1,3}(\\.\\d{1,3}){3}$/.test(host)) return \"\";\n\n const parts = host.split(\".\");\n // 形如 example.com(两段):父域就是它自己。\n if (parts.length <= 2) return host;\n // 形如 a.example.com:去掉最左一段。\n return parts.slice(1).join(\".\");\n}\n\n/**\n * 读取指定名称的 cookie 值。\n * @param {string} name\n * @returns {string|null}\n */\nexport function readCookie(name) {\n const prefix = encodeURIComponent(name) + \"=\";\n const items = document.cookie ? document.cookie.split(\"; \") : [];\n for (const item of items) {\n if (item.indexOf(prefix) === 0) {\n return decodeURIComponent(item.slice(prefix.length));\n }\n }\n return null;\n}\n\n/**\n * 写入跨子域 cookie。\n *\n * @param {string} name\n * @param {string} value\n * @param {object} opts\n * @param {string} opts.domain cookie 的 Domain,传空串则不写 Domain(仅当前 host)\n * @param {number} opts.maxAgeSeconds 有效期(秒)\n * @param {string} [opts.sameSite=\"Lax\"]\n * @param {boolean} [opts.secure=true] HTTPS 下应为 true;本地 http 调试时会自动放宽\n * @param {string} [opts.path=\"/\"]\n */\nexport function writeCookie(name, value, opts) {\n const {\n domain,\n maxAgeSeconds,\n sameSite = \"Lax\",\n path = \"/\",\n } = opts;\n\n // 本地 http 调试时 Secure 会让 cookie 写不进去,这里按当前协议自动判断。\n const secure = opts.secure !== undefined\n ? opts.secure\n : window.location.protocol === \"https:\";\n\n let str =\n encodeURIComponent(name) + \"=\" + encodeURIComponent(value) +\n \"; path=\" + path +\n \"; max-age=\" + Math.floor(maxAgeSeconds) +\n \"; samesite=\" + sameSite;\n\n if (domain) str += \"; domain=\" + domain;\n if (secure) str += \"; secure\";\n\n document.cookie = str;\n}\n\n/**\n * 删除跨子域 cookie(用于「登出」)。Domain / Path 必须和写入时一致才能删掉。\n * @param {string} name\n * @param {object} opts\n * @param {string} opts.domain\n * @param {string} [opts.path=\"/\"]\n */\nexport function deleteCookie(name, opts) {\n const { domain, path = \"/\" } = opts;\n let str = encodeURIComponent(name) + \"=; path=\" + path + \"; max-age=0\";\n if (domain) str += \"; domain=\" + domain;\n document.cookie = str;\n}\n","// 第二/三级:向真实校验后端(gate-auth-server)校验密码与 token。\n//\n// 设计要点:真密码只存在后端,前端 bundle 里没有任何密码信息。\n// 校验通过后端会签发一枚 JWT,前端把它存进 cookie;再次进入时调 /check\n// 验签——攻击者没有服务端密钥,签不出合法 token,伪造 cookie 进不来。\n\n/**\n * 密码校验结果。\n * @typedef {object} VerifyResult\n * @property {boolean} ok 密码是否正确\n * @property {string|null} [token] 后端签发的 JWT(ok 时有值),写入 cookie\n * @property {boolean} [networkError] 是否因网络/接口异常导致(区别于「密码错」)\n * @property {string} [message] 可展示给用户的提示\n */\n\n/**\n * 第二级:调 `{baseUrl}/verify` 校验密码。\n *\n * 接口约定:\n * POST {baseUrl}/verify body: {\"password\":\"用户输入\"}\n * 通过 HTTP 200, { code:0, data:{ match:true, token:\"<JWT>\", expiresIn:604800 } }\n * 密码错 HTTP 401, { code:1, data:{ match:false } }\n *\n * 判定规则:只有「HTTP ok 且 data.match === true」才算通过;其余一律当密码错。\n * 网络层异常单独标记 networkError,便于 UI 区分提示。\n *\n * @param {string} baseUrl 真实校验后端地址(不含末尾斜杠)\n * @param {string} password\n * @param {object} [opts]\n * @param {number} [opts.timeoutMs=10000]\n * @returns {Promise<VerifyResult>}\n */\nexport async function verifyPassword(baseUrl, password, opts = {}) {\n const { timeoutMs = 10000 } = opts;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let resp;\n try {\n resp = await fetch(`${baseUrl}/verify`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ password }),\n signal: controller.signal,\n });\n } catch {\n clearTimeout(timer);\n // fetch 抛错只可能是网络层问题(断网、CORS、超时 abort 等),不是密码错。\n return { ok: false, networkError: true, message: \"网络异常,请稍后重试\" };\n }\n clearTimeout(timer);\n\n let data = null;\n try {\n data = await resp.json();\n } catch {\n // 响应不是合法 JSON:当作密码错/异常处理,不放行。\n data = null;\n }\n\n const match = !!(resp.ok && data && data.data && data.data.match === true);\n\n if (match) {\n return { ok: true, token: (data.data && data.data.token) || null };\n }\n\n return { ok: false, networkError: false, message: \"密码错误\" };\n}\n\n/**\n * 第三级:调 `{baseUrl}/check` 验签 cookie 里的 JWT。\n *\n * 接口约定:\n * POST {baseUrl}/check header: { Authorization: \"Bearer <token>\" }\n * 有效 HTTP 200, { code:0, data:{ valid:true } }\n * 无效/过期/伪造 HTTP 401, { code:1, data:{ valid:false } }\n *\n * 用于「已有 cookie」分支:验过才放行,否则重新弹密码框。\n * 网络异常时返回 false(保守起见不放行),由调用方决定是否回退到弹框。\n *\n * @param {string} baseUrl 真实校验后端地址(不含末尾斜杠)\n * @param {string} token cookie 里存的 JWT\n * @param {object} [opts]\n * @param {number} [opts.timeoutMs=10000]\n * @returns {Promise<boolean>} token 是否有效\n */\nexport async function checkToken(baseUrl, token, opts = {}) {\n const { timeoutMs = 10000 } = opts;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let resp;\n try {\n resp = await fetch(`${baseUrl}/check`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({ token }),\n signal: controller.signal,\n });\n } catch {\n clearTimeout(timer);\n return false;\n }\n clearTimeout(timer);\n\n let data = null;\n try {\n data = await resp.json();\n } catch {\n data = null;\n }\n\n return !!(resp.ok && data && data.data && data.data.valid === true);\n}\n","// 第一级:地址分发接口。用一个固定 token 换取真实的校验后端地址。\n//\n// 为什么要这一级:避免真实校验接口地址被写死在前端 bundle 字符串里。\n// 这里的 token 不是安全凭证——它本来就会出现在前端请求里,作用只是\n// 「让真实地址不直接出现在 bundle 里」,提高一点点扒取门槛。\n// 真正的安全靠第二/三级的 JWT 校验。\n//\n// 设计选择:不在内存缓存 data.url,每次校验都重新分发拉取,\n// 这样后端在 mock 里改地址即时生效。\n\n/** 地址分发接口 URL(写死在包内,改地址只改这里并发新版本)。 */\nexport const DISCOVER_URL =\n \"https://m1.apifoxmock.com/m1/6205743-5899102-default/getWebRateAddress\";\n\n/** 换取真实地址必带的固定 token(非安全凭证,仅提高扒取门槛)。 */\nexport const DISCOVER_TOKEN = \"qB8pgQh*pcbBRf3P\";\n\n/**\n * 向地址分发接口换取真实校验后端地址。\n *\n * 接口约定:\n * GET DISCOVER_URL header: { token: DISCOVER_TOKEN }\n * 成功 HTTP 200, { code:200, data:{ url:\"真实校验地址\" } }\n * 失败 HTTP 401, { code:401, data:{} }\n *\n * @param {object} [opts]\n * @param {number} [opts.timeoutMs=10000]\n * @returns {Promise<string>} 真实校验后端地址(已去除末尾斜杠)\n * @throws {Error} 网络异常、未授权或返回里没有 url 时抛出\n */\nexport async function discoverBaseUrl(opts = {}) {\n const { timeoutMs = 10000 } = opts;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let resp;\n try {\n resp = await fetch(DISCOVER_URL, {\n method: \"GET\",\n headers: { token: DISCOVER_TOKEN },\n signal: controller.signal,\n });\n } catch {\n throw new Error(\"获取校验接口地址失败:网络异常\");\n } finally {\n clearTimeout(timer);\n }\n\n let data = null;\n try {\n data = await resp.json();\n } catch {\n data = null;\n }\n\n const url = data && data.data && data.data.url;\n if (!resp.ok || data.code !== 200 || !url) {\n throw new Error(\"获取校验接口地址失败\");\n }\n\n // 去掉末尾斜杠,方便后续拼 `${base}/verify`、`${base}/check`。\n return String(url).replace(/\\/+$/, \"\");\n}\n","// 框架无关的密码输入页(原生 DOM)。\n//\n// 用 Shadow DOM 把样式和结构完全隔离,既不会污染子站的 CSS,\n// 也不会被子站的全局样式影响。任何前端项目(Vue / React / 纯 HTML)都能用。\n\nconst STYLE = `\n:host { all: initial; }\n.mask {\n position: fixed; inset: 0; z-index: 2147483647;\n display: flex; align-items: center; justify-content: center;\n background: #0f1115;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n}\n.card {\n width: 320px; max-width: calc(100vw - 48px);\n padding: 32px 28px;\n background: #1b1e25; border-radius: 14px;\n box-shadow: 0 12px 40px rgba(0,0,0,.45);\n box-sizing: border-box;\n}\n.title { margin: 0 0 6px; font-size: 18px; font-weight: 600; color: #f2f4f8; }\n.subtitle { margin: 0 0 22px; font-size: 13px; color: #8a92a6; line-height: 1.5; }\n.field { position: relative; }\n.input {\n width: 100%; box-sizing: border-box;\n padding: 11px 13px; font-size: 14px;\n color: #f2f4f8; background: #11141a;\n border: 1px solid #2c313c; border-radius: 9px; outline: none;\n transition: border-color .15s;\n}\n.input:focus { border-color: #4c8dff; }\n.btn {\n width: 100%; margin-top: 14px; padding: 11px 0;\n font-size: 14px; font-weight: 600; color: #fff; cursor: pointer;\n background: #4c8dff; border: none; border-radius: 9px;\n transition: background .15s, opacity .15s;\n}\n.btn:hover { background: #3a7df0; }\n.btn:disabled { opacity: .6; cursor: not-allowed; }\n.error {\n min-height: 18px; margin-top: 12px;\n font-size: 12.5px; color: #ff6b6b; line-height: 1.4;\n}\n`;\n\n/**\n * 创建并挂载密码页。返回一个对象,可用于销毁。\n *\n * @param {object} cfg\n * @param {string} cfg.title\n * @param {string} cfg.subtitle\n * @param {string} cfg.placeholder\n * @param {string} cfg.buttonText\n * @param {(password: string) => Promise<{ok: boolean, message?: string}>} cfg.onSubmit\n * 提交回调;返回 ok=true 时 UI 自动销毁,false 时显示 message。\n * @returns {{ destroy: () => void }}\n */\nexport function mountPasswordGate(cfg) {\n const host = document.createElement(\"div\");\n host.setAttribute(\"data-zy-web-gate\", \"\");\n const shadow = host.attachShadow({ mode: \"open\" });\n\n const style = document.createElement(\"style\");\n style.textContent = STYLE;\n shadow.appendChild(style);\n\n const mask = document.createElement(\"div\");\n mask.className = \"mask\";\n mask.innerHTML = `\n <div class=\"card\">\n <h1 class=\"title\"></h1>\n <p class=\"subtitle\"></p>\n <div class=\"field\">\n <input class=\"input\" type=\"password\" autocomplete=\"current-password\" />\n </div>\n <button class=\"btn\" type=\"button\"></button>\n <div class=\"error\" aria-live=\"polite\"></div>\n </div>\n `;\n shadow.appendChild(mask);\n\n // 用 textContent 赋值,避免把配置文本当 HTML 注入。\n shadow.querySelector(\".title\").textContent = cfg.title;\n shadow.querySelector(\".subtitle\").textContent = cfg.subtitle;\n const input = shadow.querySelector(\".input\");\n const btn = shadow.querySelector(\".btn\");\n const errorEl = shadow.querySelector(\".error\");\n input.placeholder = cfg.placeholder;\n btn.textContent = cfg.buttonText;\n\n let submitting = false;\n\n async function submit() {\n if (submitting) return;\n const password = input.value;\n if (!password) {\n errorEl.textContent = \"请输入密码\";\n return;\n }\n\n submitting = true;\n btn.disabled = true;\n errorEl.textContent = \"\";\n const originalBtnText = btn.textContent;\n btn.textContent = \"验证中…\";\n\n let result;\n try {\n result = await cfg.onSubmit(password);\n } finally {\n submitting = false;\n btn.disabled = false;\n btn.textContent = originalBtnText;\n }\n\n if (result && result.ok) {\n destroy();\n } else {\n errorEl.textContent = (result && result.message) || \"密码错误\";\n input.select();\n }\n }\n\n btn.addEventListener(\"click\", submit);\n input.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") submit();\n });\n\n function destroy() {\n host.remove();\n }\n\n document.body.appendChild(host);\n // 自动聚焦输入框,方便直接打字。\n setTimeout(() => input.focus(), 0);\n\n return { destroy };\n}\n","// zy-web-gate —— 纯前端「共享密码」页面访问门(两级接口 + JWT 方案)。\n//\n// 用法(在 Vue/React/任意前端入口、挂载真实应用之前调用):\n//\n// import { ensureGate } from \"zy-web-gate\";\n//\n// await ensureGate(); // 接口已固定在包内,零参数即可\n//\n// // 走到这里说明已通过门禁,再启动真实应用:\n// createApp(App).mount(\"#app\");\n//\n// 流程(详见 INTEGRATION.md):\n// 1. 已有 cookie(JWT)?-> 调 /check 验签,过则直接放行(无 UI)。\n// 2. 否则:GET 地址分发接口换真实校验地址 -> 弹密码框 -> POST /verify。\n// 3. 通过 -> 把后端签发的 JWT 写进父域 cookie -> 放行。\n//\n// 跨子域:验证通过后写父域 cookie(Domain=example.com),同主域其他子站\n// 读到该 cookie 并验签通过即直接放行,无需再次输入密码。\n//\n// 安全边界:纯前端门禁防的是「伪造 cookie 进 UI」与「照搬旧 =1 绕过」;\n// 挡不住绕过 UI 直接扒静态资源 / 直接打业务 API——真正的安全必须靠\n// 业务数据接口自己校验 token(即第三级 /check)。务必全链路走 HTTPS。\n\nimport { inferParentDomain, readCookie, writeCookie, deleteCookie } from \"./cookie.js\";\nimport { verifyPassword, checkToken } from \"./verify.js\";\nimport { discoverBaseUrl } from \"./discover.js\";\nimport { mountPasswordGate } from \"./ui.js\";\n\n/** @type {Required<GateOptions>} 默认配置 */\nconst DEFAULTS = {\n cookieName: \"zy_web_gate\",\n cookieDomain: undefined, // undefined 表示自动推断父域\n maxAgeDays: 7,\n sameSite: \"Lax\",\n secure: undefined, // undefined 表示按当前协议自动判断\n title: \"访问验证\",\n subtitle: \"请输入访问密码后继续。\",\n placeholder: \"访问密码\",\n buttonText: \"进入\",\n timeoutMs: 10000,\n};\n\n/**\n * @typedef {object} GateOptions\n * @property {string} [cookieName] 登录态 cookie 名,默认 \"zy_web_gate\";值为后端签发的 JWT\n * @property {string} [cookieDomain] 显式指定父域;不传则自动推断(如 example.com)\n * @property {number} [maxAgeDays] 登录态有效天数,默认 7\n * @property {string} [sameSite] cookie SameSite,默认 \"Lax\"\n * @property {boolean} [secure] cookie Secure;不传则按当前协议自动判断\n * @property {string} [title] 密码页标题\n * @property {string} [subtitle] 密码页副标题\n * @property {string} [placeholder] 输入框占位文案\n * @property {string} [buttonText] 按钮文案\n * @property {number} [timeoutMs] 接口超时毫秒,默认 10000\n */\n\n/**\n * 解析最终配置。\n * @param {GateOptions} [options]\n */\nfunction resolveConfig(options) {\n const cfg = { ...DEFAULTS, ...(options || {}) };\n if (cfg.cookieDomain === undefined) {\n cfg.cookieDomain = inferParentDomain();\n }\n return cfg;\n}\n\n/**\n * 访问门主入口。\n *\n * 行为:\n * 1. 父域 cookie 已存在 JWT -> 换真实地址 -> 调 /check 验签;过则放行(无 UI)。\n * 验签不过 / 网络异常 -> 视作未登录,进入弹框流程。\n * 2. 弹出密码页,等用户输入并通过 /verify 校验。\n * 3. 校验通过 -> 把后端签发的 JWT 写进父域 cookie -> resolve。\n *\n * 该 Promise 只在「已通过门禁」时 resolve;密码未通过时不会 resolve,\n * 因此把它放在 createApp().mount() 之前 await,可保证未通过时真实应用绝不挂载。\n *\n * @param {GateOptions} [options]\n * @returns {Promise<void>}\n */\nexport async function ensureGate(options) {\n const cfg = resolveConfig(options);\n\n // 已有 cookie:取出 JWT,换真实地址后调 /check 验签——验过才放行。\n // 这一步是「JWT 方案」相对旧 =1 方案的关键:仅 cookie 存在不够,必须验签,\n // 否则攻击者随便塞个字符串就能进 UI。\n const existing = readCookie(cfg.cookieName);\n if (existing) {\n try {\n const baseUrl = await discoverBaseUrl({ timeoutMs: cfg.timeoutMs });\n if (await checkToken(baseUrl, existing, { timeoutMs: cfg.timeoutMs })) {\n return; // 验签通过,放行。\n }\n } catch {\n // 换地址失败:保守起见不放行,落到下方弹框流程。\n }\n // 验签未过:清掉无效 cookie,避免每次都白跑一遍 /check。\n deleteCookie(cfg.cookieName, { domain: cfg.cookieDomain });\n }\n\n return new Promise((resolve) => {\n mountPasswordGate({\n title: cfg.title,\n subtitle: cfg.subtitle,\n placeholder: cfg.placeholder,\n buttonText: cfg.buttonText,\n onSubmit: async (password) => {\n let baseUrl;\n try {\n baseUrl = await discoverBaseUrl({ timeoutMs: cfg.timeoutMs });\n } catch {\n return { ok: false, message: \"网络异常,请稍后重试\" };\n }\n\n const result = await verifyPassword(baseUrl, password, {\n timeoutMs: cfg.timeoutMs,\n });\n\n if (result.ok && result.token) {\n writeCookie(cfg.cookieName, result.token, {\n domain: cfg.cookieDomain,\n maxAgeSeconds: cfg.maxAgeDays * 24 * 60 * 60,\n sameSite: cfg.sameSite,\n secure: cfg.secure,\n });\n // 通过门禁:UI 会在本回调返回 ok 后自动销毁,这里同时 resolve 外层 Promise,\n // 让 await ensureGate() 的调用方继续往下挂载真实应用。\n resolve();\n return { ok: true };\n }\n\n // 密码对但后端没给 token:当作异常,不放行(避免写空 cookie)。\n if (result.ok && !result.token) {\n return { ok: false, message: \"登录态签发失败,请重试\" };\n }\n\n return result;\n },\n });\n });\n}\n\n/**\n * 主动登出:清除父域 cookie。调用后下次进入任一子站会重新要求输入密码。\n * @param {GateOptions} options 至少需与 ensureGate 一致的 cookieName / cookieDomain\n */\nexport function logoutGate(options = {}) {\n const cfg = { ...DEFAULTS, ...options };\n const domain = cfg.cookieDomain === undefined\n ? inferParentDomain()\n : cfg.cookieDomain;\n deleteCookie(cfg.cookieName, { domain });\n}\n\nexport { inferParentDomain, readCookie } from \"./cookie.js\";\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zy-web-gate",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Framework-agnostic, pure-frontend shared-password page gate with cross-subdomain login state via a parent-domain cookie. No backend auth, no accounts — verification is delegated to a check-password API.",
5
5
  "type": "module",
6
6
  "main": "./dist/zy-web-gate.umd.cjs",
@@ -20,7 +20,7 @@
20
20
  "README.md"
21
21
  ],
22
22
  "scripts": {
23
- "check": "node --check src/index.js && node --check src/cookie.js && node --check src/verify.js && node --check src/ui.js",
23
+ "check": "node --check src/index.js && node --check src/cookie.js && node --check src/verify.js && node --check src/discover.js && node --check src/ui.js",
24
24
  "build": "vite build && npm run build:types",
25
25
  "build:types": "tsc -p tsconfig.json",
26
26
  "prepublishOnly": "npm run build"