@xrkseek/harness 0.7.0-rc.2 → 0.7.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.
Files changed (200) hide show
  1. package/node_modules/@xrkseek/exec-fs/dist/present.d.ts +1 -1
  2. package/node_modules/@xrkseek/exec-fs/dist/present.d.ts.map +1 -1
  3. package/node_modules/@xrkseek/exec-fs/dist/present.js +51 -3
  4. package/node_modules/@xrkseek/exec-fs/dist/present.js.map +1 -1
  5. package/node_modules/@xrkseek/exec-memory/dist/index.d.ts +1 -0
  6. package/node_modules/@xrkseek/exec-memory/dist/index.d.ts.map +1 -1
  7. package/node_modules/@xrkseek/exec-memory/dist/index.js +1 -0
  8. package/node_modules/@xrkseek/exec-memory/dist/index.js.map +1 -1
  9. package/node_modules/@xrkseek/exec-memory/dist/layered.d.ts.map +1 -1
  10. package/node_modules/@xrkseek/exec-memory/dist/layered.js +10 -0
  11. package/node_modules/@xrkseek/exec-memory/dist/layered.js.map +1 -1
  12. package/node_modules/@xrkseek/exec-memory/dist/live-fragment.d.ts.map +1 -1
  13. package/node_modules/@xrkseek/exec-memory/dist/live-fragment.js +3 -1
  14. package/node_modules/@xrkseek/exec-memory/dist/live-fragment.js.map +1 -1
  15. package/node_modules/@xrkseek/exec-memory/dist/sqlite.d.ts.map +1 -1
  16. package/node_modules/@xrkseek/exec-memory/dist/sqlite.js +16 -2
  17. package/node_modules/@xrkseek/exec-memory/dist/sqlite.js.map +1 -1
  18. package/node_modules/@xrkseek/exec-memory/dist/store.d.ts +7 -0
  19. package/node_modules/@xrkseek/exec-memory/dist/store.d.ts.map +1 -1
  20. package/node_modules/@xrkseek/exec-memory/dist/store.js +11 -0
  21. package/node_modules/@xrkseek/exec-memory/dist/store.js.map +1 -1
  22. package/node_modules/@xrkseek/preset-harness/dist/preset.d.ts.map +1 -1
  23. package/node_modules/@xrkseek/preset-harness/dist/preset.js +2 -4
  24. package/node_modules/@xrkseek/preset-harness/dist/preset.js.map +1 -1
  25. package/node_modules/@xrkseek/preset-harness/dist/tsconfig.tsbuildinfo +1 -1
  26. package/node_modules/@xrkseek/preset-minimal/dist/tsconfig.tsbuildinfo +1 -1
  27. package/node_modules/@xrkseek/preset-server/dist/tsconfig.tsbuildinfo +1 -1
  28. package/node_modules/@xrkseek/server-face/dist/agent-home.d.ts.map +1 -1
  29. package/node_modules/@xrkseek/server-face/dist/agent-home.js +2 -5
  30. package/node_modules/@xrkseek/server-face/dist/agent-home.js.map +1 -1
  31. package/node_modules/@xrkseek/server-face/dist/agent-roster-store.d.ts +8 -1
  32. package/node_modules/@xrkseek/server-face/dist/agent-roster-store.d.ts.map +1 -1
  33. package/node_modules/@xrkseek/server-face/dist/agent-roster-store.js +10 -2
  34. package/node_modules/@xrkseek/server-face/dist/agent-roster-store.js.map +1 -1
  35. package/node_modules/@xrkseek/server-face/dist/agent-roster-tools.d.ts +2 -2
  36. package/node_modules/@xrkseek/server-face/dist/agent-roster-tools.d.ts.map +1 -1
  37. package/node_modules/@xrkseek/server-face/dist/agent-roster-tools.js +2 -1
  38. package/node_modules/@xrkseek/server-face/dist/agent-roster-tools.js.map +1 -1
  39. package/node_modules/@xrkseek/server-face/dist/agent-team-roles.d.ts.map +1 -1
  40. package/node_modules/@xrkseek/server-face/dist/agent-team-roles.js +6 -4
  41. package/node_modules/@xrkseek/server-face/dist/agent-team-roles.js.map +1 -1
  42. package/node_modules/@xrkseek/server-face/dist/catalog-seed-labels.d.ts +31 -0
  43. package/node_modules/@xrkseek/server-face/dist/catalog-seed-labels.d.ts.map +1 -0
  44. package/node_modules/@xrkseek/server-face/dist/catalog-seed-labels.js +106 -0
  45. package/node_modules/@xrkseek/server-face/dist/catalog-seed-labels.js.map +1 -0
  46. package/node_modules/@xrkseek/server-face/dist/context.d.ts +14 -3
  47. package/node_modules/@xrkseek/server-face/dist/context.d.ts.map +1 -1
  48. package/node_modules/@xrkseek/server-face/dist/handlers/catalog.d.ts.map +1 -1
  49. package/node_modules/@xrkseek/server-face/dist/handlers/catalog.js +11 -0
  50. package/node_modules/@xrkseek/server-face/dist/handlers/catalog.js.map +1 -1
  51. package/node_modules/@xrkseek/server-face/dist/handlers/session.d.ts.map +1 -1
  52. package/node_modules/@xrkseek/server-face/dist/handlers/session.js +27 -1
  53. package/node_modules/@xrkseek/server-face/dist/handlers/session.js.map +1 -1
  54. package/node_modules/@xrkseek/server-face/dist/handlers/subagent.d.ts.map +1 -1
  55. package/node_modules/@xrkseek/server-face/dist/handlers/subagent.js +14 -4
  56. package/node_modules/@xrkseek/server-face/dist/handlers/subagent.js.map +1 -1
  57. package/node_modules/@xrkseek/server-face/dist/handlers/team.d.ts.map +1 -1
  58. package/node_modules/@xrkseek/server-face/dist/handlers/team.js +76 -12
  59. package/node_modules/@xrkseek/server-face/dist/handlers/team.js.map +1 -1
  60. package/node_modules/@xrkseek/server-face/dist/index.d.ts +5 -3
  61. package/node_modules/@xrkseek/server-face/dist/index.d.ts.map +1 -1
  62. package/node_modules/@xrkseek/server-face/dist/index.js +5 -3
  63. package/node_modules/@xrkseek/server-face/dist/index.js.map +1 -1
  64. package/node_modules/@xrkseek/server-face/dist/projections/title-controller.d.ts +6 -0
  65. package/node_modules/@xrkseek/server-face/dist/projections/title-controller.d.ts.map +1 -1
  66. package/node_modules/@xrkseek/server-face/dist/projections/title-controller.js +26 -0
  67. package/node_modules/@xrkseek/server-face/dist/projections/title-controller.js.map +1 -1
  68. package/node_modules/@xrkseek/server-face/dist/runtime.d.ts.map +1 -1
  69. package/node_modules/@xrkseek/server-face/dist/runtime.js +72 -21
  70. package/node_modules/@xrkseek/server-face/dist/runtime.js.map +1 -1
  71. package/node_modules/@xrkseek/server-face/dist/seat-channel.d.ts +45 -0
  72. package/node_modules/@xrkseek/server-face/dist/seat-channel.d.ts.map +1 -0
  73. package/node_modules/@xrkseek/server-face/dist/seat-channel.js +93 -0
  74. package/node_modules/@xrkseek/server-face/dist/seat-channel.js.map +1 -0
  75. package/node_modules/@xrkseek/server-face/dist/seat-identity.d.ts +13 -0
  76. package/node_modules/@xrkseek/server-face/dist/seat-identity.d.ts.map +1 -1
  77. package/node_modules/@xrkseek/server-face/dist/seat-identity.js +90 -36
  78. package/node_modules/@xrkseek/server-face/dist/seat-identity.js.map +1 -1
  79. package/node_modules/@xrkseek/server-face/dist/session-capability.d.ts.map +1 -1
  80. package/node_modules/@xrkseek/server-face/dist/session-capability.js +4 -1
  81. package/node_modules/@xrkseek/server-face/dist/session-capability.js.map +1 -1
  82. package/node_modules/@xrkseek/server-face/dist/session-status.d.ts +4 -0
  83. package/node_modules/@xrkseek/server-face/dist/session-status.d.ts.map +1 -1
  84. package/node_modules/@xrkseek/server-face/dist/session-status.js +93 -51
  85. package/node_modules/@xrkseek/server-face/dist/session-status.js.map +1 -1
  86. package/node_modules/@xrkseek/server-face/dist/settings-credentials.d.ts +5 -0
  87. package/node_modules/@xrkseek/server-face/dist/settings-credentials.d.ts.map +1 -1
  88. package/node_modules/@xrkseek/server-face/dist/settings-credentials.js +20 -0
  89. package/node_modules/@xrkseek/server-face/dist/settings-credentials.js.map +1 -1
  90. package/node_modules/@xrkseek/server-face/dist/settings-document.d.ts.map +1 -1
  91. package/node_modules/@xrkseek/server-face/dist/settings-document.js +34 -0
  92. package/node_modules/@xrkseek/server-face/dist/settings-document.js.map +1 -1
  93. package/node_modules/@xrkseek/server-face/dist/settings-schemas.d.ts.map +1 -1
  94. package/node_modules/@xrkseek/server-face/dist/settings-schemas.js +18 -11
  95. package/node_modules/@xrkseek/server-face/dist/settings-schemas.js.map +1 -1
  96. package/node_modules/@xrkseek/server-face/dist/workspace-face.d.ts.map +1 -1
  97. package/node_modules/@xrkseek/server-face/dist/workspace-face.js +3 -10
  98. package/node_modules/@xrkseek/server-face/dist/workspace-face.js.map +1 -1
  99. package/node_modules/@xrkseek/server-face/dist/workspace-member-look.d.ts +1 -1
  100. package/node_modules/@xrkseek/server-face/dist/workspace-member-look.d.ts.map +1 -1
  101. package/node_modules/@xrkseek/server-face/dist/workspace-member-look.js +4 -1
  102. package/node_modules/@xrkseek/server-face/dist/workspace-member-look.js.map +1 -1
  103. package/node_modules/@xrkseek/server-face/dist/xiangyang-skill-bodies/index.d.ts +1 -1
  104. package/node_modules/@xrkseek/server-face/dist/xiangyang-skill-bodies/index.js +1 -1
  105. package/node_modules/@xrkseek/server-face/dist/xiangyang-skill-bodies/memory-polish.d.ts +2 -2
  106. package/node_modules/@xrkseek/server-face/dist/xiangyang-skill-bodies/memory-polish.d.ts.map +1 -1
  107. package/node_modules/@xrkseek/server-face/dist/xiangyang-skill-bodies/memory-polish.js +27 -13
  108. package/node_modules/@xrkseek/server-face/dist/xiangyang-skill-bodies/memory-polish.js.map +1 -1
  109. package/node_modules/@xrkseek/server-host/dist/community-live-bridge.d.ts +14 -0
  110. package/node_modules/@xrkseek/server-host/dist/community-live-bridge.d.ts.map +1 -0
  111. package/node_modules/@xrkseek/server-host/dist/community-live-bridge.js +149 -0
  112. package/node_modules/@xrkseek/server-host/dist/community-live-bridge.js.map +1 -0
  113. package/node_modules/@xrkseek/server-host/dist/index.d.ts.map +1 -1
  114. package/node_modules/@xrkseek/server-host/dist/index.js +13 -1
  115. package/node_modules/@xrkseek/server-host/dist/index.js.map +1 -1
  116. package/node_modules/@xrkseek/server-http/dist/dsh-compat/adapter-providers.d.ts.map +1 -1
  117. package/node_modules/@xrkseek/server-http/dist/dsh-compat/adapter-providers.js +15 -0
  118. package/node_modules/@xrkseek/server-http/dist/dsh-compat/adapter-providers.js.map +1 -1
  119. package/node_modules/@xrkseek/server-http/dist/dsh-compat/adapter-types.d.ts +11 -0
  120. package/node_modules/@xrkseek/server-http/dist/dsh-compat/adapter-types.d.ts.map +1 -1
  121. package/node_modules/@xrkseek/server-http/dist/dsh-compat/community-live-state.d.ts +38 -0
  122. package/node_modules/@xrkseek/server-http/dist/dsh-compat/community-live-state.d.ts.map +1 -0
  123. package/node_modules/@xrkseek/server-http/dist/dsh-compat/community-live-state.js +182 -0
  124. package/node_modules/@xrkseek/server-http/dist/dsh-compat/community-live-state.js.map +1 -0
  125. package/node_modules/@xrkseek/server-http/dist/dsh-compat/community-root-http.d.ts +2 -1
  126. package/node_modules/@xrkseek/server-http/dist/dsh-compat/community-root-http.d.ts.map +1 -1
  127. package/node_modules/@xrkseek/server-http/dist/dsh-compat/community-root-http.js +26 -2
  128. package/node_modules/@xrkseek/server-http/dist/dsh-compat/community-root-http.js.map +1 -1
  129. package/node_modules/@xrkseek/server-http/dist/dsh-compat/cordis-connection-bridge.d.ts.map +1 -1
  130. package/node_modules/@xrkseek/server-http/dist/dsh-compat/cordis-connection-bridge.js +29 -4
  131. package/node_modules/@xrkseek/server-http/dist/dsh-compat/cordis-connection-bridge.js.map +1 -1
  132. package/node_modules/@xrkseek/server-http/dist/dsh-compat/dsh-compat-matrix.js +1 -1
  133. package/node_modules/@xrkseek/server-http/dist/dsh-compat/dsh-compat-matrix.js.map +1 -1
  134. package/node_modules/@xrkseek/server-http/dist/dsh-compat/dsh-path-capabilities.d.ts.map +1 -1
  135. package/node_modules/@xrkseek/server-http/dist/dsh-compat/dsh-path-capabilities.js +5 -2
  136. package/node_modules/@xrkseek/server-http/dist/dsh-compat/dsh-path-capabilities.js.map +1 -1
  137. package/node_modules/@xrkseek/server-http/dist/dsh-compat/dsh-skins.d.ts.map +1 -1
  138. package/node_modules/@xrkseek/server-http/dist/dsh-compat/dsh-skins.js +2 -0
  139. package/node_modules/@xrkseek/server-http/dist/dsh-compat/dsh-skins.js.map +1 -1
  140. package/node_modules/@xrkseek/server-http/dist/dsh-compat/honest-http-catchall.d.ts.map +1 -1
  141. package/node_modules/@xrkseek/server-http/dist/dsh-compat/honest-http-catchall.js +13 -3
  142. package/node_modules/@xrkseek/server-http/dist/dsh-compat/honest-http-catchall.js.map +1 -1
  143. package/node_modules/@xrkseek/server-http/dist/dsh-compat/incomplete-api-shapes.d.ts +21 -0
  144. package/node_modules/@xrkseek/server-http/dist/dsh-compat/incomplete-api-shapes.d.ts.map +1 -0
  145. package/node_modules/@xrkseek/server-http/dist/dsh-compat/incomplete-api-shapes.js +62 -0
  146. package/node_modules/@xrkseek/server-http/dist/dsh-compat/incomplete-api-shapes.js.map +1 -0
  147. package/node_modules/@xrkseek/server-http/dist/dsh-compat/index.d.ts +5 -2
  148. package/node_modules/@xrkseek/server-http/dist/dsh-compat/index.d.ts.map +1 -1
  149. package/node_modules/@xrkseek/server-http/dist/dsh-compat/index.js +6 -2
  150. package/node_modules/@xrkseek/server-http/dist/dsh-compat/index.js.map +1 -1
  151. package/node_modules/@xrkseek/server-http/dist/dsh-compat/mobile-access-auth.d.ts +19 -2
  152. package/node_modules/@xrkseek/server-http/dist/dsh-compat/mobile-access-auth.d.ts.map +1 -1
  153. package/node_modules/@xrkseek/server-http/dist/dsh-compat/mobile-access-auth.js +425 -385
  154. package/node_modules/@xrkseek/server-http/dist/dsh-compat/mobile-access-auth.js.map +1 -1
  155. package/node_modules/@xrkseek/server-http/dist/dsh-compat/mobile-access-gate.d.ts +20 -1
  156. package/node_modules/@xrkseek/server-http/dist/dsh-compat/mobile-access-gate.d.ts.map +1 -1
  157. package/node_modules/@xrkseek/server-http/dist/dsh-compat/mobile-access-gate.js +22 -1
  158. package/node_modules/@xrkseek/server-http/dist/dsh-compat/mobile-access-gate.js.map +1 -1
  159. package/node_modules/@xrkseek/server-http/dist/dsh-compat/mobile-access.d.ts +13 -1
  160. package/node_modules/@xrkseek/server-http/dist/dsh-compat/mobile-access.d.ts.map +1 -1
  161. package/node_modules/@xrkseek/server-http/dist/dsh-compat/mobile-access.js +37 -3
  162. package/node_modules/@xrkseek/server-http/dist/dsh-compat/mobile-access.js.map +1 -1
  163. package/node_modules/@xrkseek/server-http/dist/dsh-compat/our-free-model-host.d.ts.map +1 -1
  164. package/node_modules/@xrkseek/server-http/dist/dsh-compat/our-free-model-host.js +2 -1
  165. package/node_modules/@xrkseek/server-http/dist/dsh-compat/our-free-model-host.js.map +1 -1
  166. package/node_modules/@xrkseek/server-http/dist/dsh-compat/plugin-package-id.d.ts +22 -0
  167. package/node_modules/@xrkseek/server-http/dist/dsh-compat/plugin-package-id.d.ts.map +1 -0
  168. package/node_modules/@xrkseek/server-http/dist/dsh-compat/plugin-package-id.js +42 -0
  169. package/node_modules/@xrkseek/server-http/dist/dsh-compat/plugin-package-id.js.map +1 -0
  170. package/node_modules/@xrkseek/server-http/dist/dsh-compat/skin-center.d.ts +1 -1
  171. package/node_modules/@xrkseek/server-http/dist/dsh-compat/skin-discover.d.ts +2 -0
  172. package/node_modules/@xrkseek/server-http/dist/dsh-compat/skin-discover.d.ts.map +1 -1
  173. package/node_modules/@xrkseek/server-http/dist/dsh-compat/skin-discover.js +5 -0
  174. package/node_modules/@xrkseek/server-http/dist/dsh-compat/skin-discover.js.map +1 -1
  175. package/node_modules/@xrkseek/server-http/dist/dsh-compat/underlying/mobile-gate-kit.d.ts +4 -1
  176. package/node_modules/@xrkseek/server-http/dist/dsh-compat/underlying/mobile-gate-kit.d.ts.map +1 -1
  177. package/node_modules/@xrkseek/server-http/dist/dsh-compat/underlying/mobile-gate-kit.js +20 -8
  178. package/node_modules/@xrkseek/server-http/dist/dsh-compat/underlying/mobile-gate-kit.js.map +1 -1
  179. package/node_modules/@xrkseek/server-http/dist/dsh-compat/wallet.d.ts.map +1 -1
  180. package/node_modules/@xrkseek/server-http/dist/dsh-compat/wallet.js +2 -0
  181. package/node_modules/@xrkseek/server-http/dist/dsh-compat/wallet.js.map +1 -1
  182. package/node_modules/@xrkseek/server-http/dist/dsh-compat/wallpaper-bg.d.ts +7 -0
  183. package/node_modules/@xrkseek/server-http/dist/dsh-compat/wallpaper-bg.d.ts.map +1 -0
  184. package/node_modules/@xrkseek/server-http/dist/dsh-compat/wallpaper-bg.js +153 -0
  185. package/node_modules/@xrkseek/server-http/dist/dsh-compat/wallpaper-bg.js.map +1 -0
  186. package/node_modules/@xrkseek/server-http/dist/dsh-compat/wallpaper.d.ts +10 -2
  187. package/node_modules/@xrkseek/server-http/dist/dsh-compat/wallpaper.d.ts.map +1 -1
  188. package/node_modules/@xrkseek/server-http/dist/dsh-compat/wallpaper.js +361 -29
  189. package/node_modules/@xrkseek/server-http/dist/dsh-compat/wallpaper.js.map +1 -1
  190. package/node_modules/@xrkseek/server-http/dist/dsh-compat/wire-normalize.d.ts.map +1 -1
  191. package/node_modules/@xrkseek/server-http/dist/dsh-compat/wire-normalize.js +3 -0
  192. package/node_modules/@xrkseek/server-http/dist/dsh-compat/wire-normalize.js.map +1 -1
  193. package/node_modules/@xrkseek/server-http/dist/dsh-compat/workbuddy-host.d.ts.map +1 -1
  194. package/node_modules/@xrkseek/server-http/dist/dsh-compat/workbuddy-host.js +2 -1
  195. package/node_modules/@xrkseek/server-http/dist/dsh-compat/workbuddy-host.js.map +1 -1
  196. package/node_modules/@xrkseek/server-http/dist/index.d.ts +1 -1
  197. package/node_modules/@xrkseek/server-http/dist/index.d.ts.map +1 -1
  198. package/node_modules/@xrkseek/server-http/dist/index.js +1 -1
  199. package/node_modules/@xrkseek/server-http/dist/index.js.map +1 -1
  200. package/package.json +1 -1
@@ -8,6 +8,13 @@ export const CSRF_COOKIE = "dsh_ma_csrf";
8
8
  export const CSRF_HEADER = "x-dsh-mobile-csrf";
9
9
  export const LAN_PIN_COOKIE = "dsh_ma_lan";
10
10
  export const WAN_PIN_COOKIE = "dsh_ma_wan";
11
+ /**
12
+ * Non-HttpOnly twin of the LAN/WAN PIN cookies. The SPA cannot read HttpOnly
13
+ * PIN digests; this marker lets `sessionStorage.xrk.operatorAccess` elevate on
14
+ * cookie-return visits so Settings → Plugins cards stay Host-backed.
15
+ * Keep in sync with client `OPERATOR_UI_COOKIE` (`@xrkseek/client-connection`).
16
+ */
17
+ export const OPERATOR_UI_COOKIE = "xrk_operator_ui";
11
18
  const DEVICE_TTL_MS = 90 * 24 * 60 * 60 * 1000;
12
19
  const SESSION_TTL_MS = 12 * 60 * 60 * 1000;
13
20
  const MAX_DEVICES = 32;
@@ -135,6 +142,7 @@ export function renewSession(state, deviceToken, now = Date.now()) {
135
142
  }
136
143
  export function setMobileSessionCookies(res, tokens, tls = false) {
137
144
  const now = Date.now();
145
+ const sessionMaxAgeSeconds = Math.floor((tokens.sessionExpiresAt - now) / 1000);
138
146
  res.setHeader("set-cookie", [
139
147
  cookie(DEVICE_COOKIE, tokens.deviceToken, {
140
148
  path: "/mobile-access/auth/renew",
@@ -144,16 +152,23 @@ export function setMobileSessionCookies(res, tokens, tls = false) {
144
152
  }),
145
153
  cookie(SESSION_COOKIE, tokens.sessionToken, {
146
154
  path: "/",
147
- maxAgeSeconds: Math.floor((tokens.sessionExpiresAt - now) / 1000),
155
+ maxAgeSeconds: sessionMaxAgeSeconds,
148
156
  httpOnly: true,
149
157
  tls,
150
158
  }),
151
159
  cookie(CSRF_COOKIE, tokens.csrfToken, {
152
160
  path: "/",
153
- maxAgeSeconds: Math.floor((tokens.sessionExpiresAt - now) / 1000),
161
+ maxAgeSeconds: sessionMaxAgeSeconds,
154
162
  httpOnly: true,
155
163
  tls,
156
164
  }),
165
+ // Same SPA elevation marker as PIN (cookie-return / pair without re-run).
166
+ cookie(OPERATOR_UI_COOKIE, "1", {
167
+ path: "/",
168
+ maxAgeSeconds: sessionMaxAgeSeconds,
169
+ httpOnly: false,
170
+ tls,
171
+ }),
157
172
  ]);
158
173
  }
159
174
  export function parseCookieHeader(header, name) {
@@ -197,13 +212,22 @@ export function verifyMobileSession(state, sessionToken, now = Date.now()) {
197
212
  }
198
213
  export function setAccessPinCookie(res, cookieName, pin, instanceId, tls = false) {
199
214
  const value = accessPinCookieDigest(pin, instanceId);
215
+ const maxAgeSeconds = Math.floor(ACCESS_PIN_TTL_MS / 1000);
200
216
  res.setHeader("set-cookie", [
201
217
  cookie(cookieName, value, {
202
218
  path: "/",
203
- maxAgeSeconds: Math.floor(ACCESS_PIN_TTL_MS / 1000),
219
+ maxAgeSeconds,
204
220
  httpOnly: true,
205
221
  tls,
206
222
  }),
223
+ // Readable by the SPA so WAN/LAN cookie-return visits elevate without
224
+ // re-running the PIN page script (which alone used to set sessionStorage).
225
+ cookie(OPERATOR_UI_COOKIE, "1", {
226
+ path: "/",
227
+ maxAgeSeconds,
228
+ httpOnly: false,
229
+ tls,
230
+ }),
207
231
  ]);
208
232
  }
209
233
  export function publicDevice(device) {
@@ -217,401 +241,417 @@ export function publicDevice(device) {
217
241
  lastSeenAt: device.lastSeenAt,
218
242
  };
219
243
  }
220
- export const PAIR_PAGE_HTML = `<!doctype html>
221
- <html lang="zh-CN">
222
- <meta charset="utf-8">
223
- <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
224
- <title>配对 XRK 移动访问</title>
225
- <main style="font-family:system-ui,sans-serif;max-width:28rem;margin:2rem auto;padding:0 1rem">
226
- <h1>配对此设备</h1>
227
- <p>输入桌面端显示的配对码,或从二维码链接自动填充。</p>
228
- <form id="pair-form">
229
- <label style="display:block;margin:.75rem 0">配对码
230
- <input id="pair-token" autocomplete="one-time-code" required style="width:100%;padding:.5rem">
231
- </label>
232
- <label style="display:block;margin:.75rem 0">设备名称
233
- <input id="device-label" maxlength="64" autocomplete="off" style="width:100%;padding:.5rem">
234
- </label>
235
- <button type="submit" style="padding:.5rem 1rem">配对</button>
236
- <output id="pair-status" style="display:block;margin-top:.75rem"></output>
237
- </form>
238
- </main>
239
- <script src="/mobile-access/pair.js" defer></script>
244
+ export const PAIR_PAGE_HTML = `<!doctype html>
245
+ <html lang="zh-CN">
246
+ <meta charset="utf-8">
247
+ <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
248
+ <title>配对 XRK 移动访问</title>
249
+ <main style="font-family:system-ui,sans-serif;max-width:28rem;margin:2rem auto;padding:0 1rem">
250
+ <h1>配对此设备</h1>
251
+ <p>输入桌面端显示的配对码,或从二维码链接自动填充。</p>
252
+ <form id="pair-form">
253
+ <label style="display:block;margin:.75rem 0">配对码
254
+ <input id="pair-token" autocomplete="one-time-code" required style="width:100%;padding:.5rem">
255
+ </label>
256
+ <label style="display:block;margin:.75rem 0">设备名称
257
+ <input id="device-label" maxlength="64" autocomplete="off" style="width:100%;padding:.5rem">
258
+ </label>
259
+ <button type="submit" style="padding:.5rem 1rem">配对</button>
260
+ <output id="pair-status" style="display:block;margin-top:.75rem"></output>
261
+ </form>
262
+ </main>
263
+ <script src="/mobile-access/pair.js" defer></script>
240
264
  </html>`;
241
- export const PAIR_PAGE_JS = `(() => {
242
- const params = new URLSearchParams(location.hash.replace(/^#/, ''))
243
- const tokenInput = document.getElementById('pair-token')
244
- const labelInput = document.getElementById('device-label')
245
- const status = document.getElementById('pair-status')
246
- const form = document.getElementById('pair-form')
247
- const hashToken = params.get('token')
248
- if (hashToken && tokenInput) tokenInput.value = hashToken
249
- form?.addEventListener('submit', async (event) => {
250
- event.preventDefault()
251
- status.textContent = '配对中…'
252
- const response = await fetch('/mobile-access/auth/pair', {
253
- method: 'POST',
254
- credentials: 'same-origin',
255
- headers: { 'content-type': 'application/json' },
256
- body: JSON.stringify({
257
- token: tokenInput?.value ?? '',
258
- label: labelInput?.value || undefined,
259
- }),
260
- })
261
- if (!response.ok) {
262
- status.textContent = '配对失败(链接可能已过期)'
263
- return
264
- }
265
- location.replace('/')
266
- })
267
- })()
265
+ export const PAIR_PAGE_JS = `(() => {
266
+ const params = new URLSearchParams(location.hash.replace(/^#/, ''))
267
+ const tokenInput = document.getElementById('pair-token')
268
+ const labelInput = document.getElementById('device-label')
269
+ const status = document.getElementById('pair-status')
270
+ const form = document.getElementById('pair-form')
271
+ const hashToken = params.get('token')
272
+ if (hashToken && tokenInput) tokenInput.value = hashToken
273
+ form?.addEventListener('submit', async (event) => {
274
+ event.preventDefault()
275
+ status.textContent = '配对中…'
276
+ const response = await fetch('/mobile-access/auth/pair', {
277
+ method: 'POST',
278
+ credentials: 'same-origin',
279
+ headers: { 'content-type': 'application/json' },
280
+ body: JSON.stringify({
281
+ token: tokenInput?.value ?? '',
282
+ label: labelInput?.value || undefined,
283
+ }),
284
+ })
285
+ if (!response.ok) {
286
+ status.textContent = '配对失败(链接可能已过期)'
287
+ return
288
+ }
289
+ try { sessionStorage.setItem('xrk.operatorAccess', '1') } catch {}
290
+ location.replace('/')
291
+ })
292
+ })()
268
293
  `;
269
- export const LOGIN_PAGE_HTML = `<!doctype html>
270
- <html lang="zh-CN">
271
- <meta charset="utf-8">
272
- <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
273
- <title>XRK 移动访问</title>
274
- <main style="font-family:system-ui,sans-serif;max-width:28rem;margin:2rem auto;padding:0 1rem">
275
- <h1>正在恢复会话…</h1>
276
- <p id="login-progress">正在验证设备凭据。</p>
277
- <p id="login-failed" hidden>无法恢复会话,请重新配对。</p>
278
- <a href="/mobile-access/pair">打开配对页</a>
279
- </main>
280
- <script src="/mobile-access/login.js" defer></script>
294
+ export const LOGIN_PAGE_HTML = `<!doctype html>
295
+ <html lang="zh-CN">
296
+ <meta charset="utf-8">
297
+ <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
298
+ <title>XRK 移动访问</title>
299
+ <main style="font-family:system-ui,sans-serif;max-width:28rem;margin:2rem auto;padding:0 1rem">
300
+ <h1>正在恢复会话…</h1>
301
+ <p id="login-progress">正在验证设备凭据。</p>
302
+ <p id="login-failed" hidden>无法恢复会话,请重新配对。</p>
303
+ <a href="/mobile-access/pair">打开配对页</a>
304
+ </main>
305
+ <script src="/mobile-access/login.js" defer></script>
281
306
  </html>`;
282
- export const LOGIN_PAGE_JS = `(() => {
283
- fetch('/mobile-access/auth/renew', {
284
- method: 'POST',
285
- credentials: 'same-origin',
286
- headers: { 'content-type': 'application/json' },
287
- body: '{}',
288
- }).then((response) => {
289
- if (response.ok) {
290
- const next = new URLSearchParams(location.search).get('return') || '/'
291
- location.replace(next)
292
- return
293
- }
294
- document.getElementById('login-progress').hidden = true
295
- document.getElementById('login-failed').hidden = false
296
- }).catch(() => {
297
- const el = document.getElementById('login-progress')
298
- if (el) el.textContent = '无法连接到主机。'
299
- })
300
- })()
307
+ export const LOGIN_PAGE_JS = `(() => {
308
+ fetch('/mobile-access/auth/renew', {
309
+ method: 'POST',
310
+ credentials: 'same-origin',
311
+ headers: { 'content-type': 'application/json' },
312
+ body: '{}',
313
+ }).then((response) => {
314
+ if (response.ok) {
315
+ try { sessionStorage.setItem('xrk.operatorAccess', '1') } catch {}
316
+ const next = new URLSearchParams(location.search).get('return') || '/'
317
+ location.replace(next)
318
+ return
319
+ }
320
+ document.getElementById('login-progress').hidden = true
321
+ document.getElementById('login-failed').hidden = false
322
+ }).catch(() => {
323
+ const el = document.getElementById('login-progress')
324
+ if (el) el.textContent = '无法连接到主机。'
325
+ })
326
+ })()
301
327
  `;
302
328
  /** Gate pages share product-shell first-paint (index.html) light/dark floors. */
303
- const GATE_SHELL_CSS = `
304
- :root {
305
- color-scheme: light dark;
306
- --gate-bg: #f4f4f5;
307
- --gate-fg: #14161a;
308
- --gate-muted: #5c6168;
309
- --gate-border: rgb(0 0 0 / 12%);
310
- --gate-fill: #14161a;
311
- --gate-fill-fg: #f4f4f5;
312
- --gate-fill-hover: #2a2d33;
313
- --gate-error: #c41e1e;
314
- --gate-field: #ffffff;
315
- --gate-ring: #5c6168;
316
- --gate-ring-dim: rgb(0 0 0 / 14%);
317
- }
318
- html[data-theme="dark"] {
319
- color-scheme: dark;
320
- --gate-bg: #151517;
321
- --gate-fg: #e8e8ea;
322
- --gate-muted: #9a9aa3;
323
- --gate-border: rgb(255 255 255 / 12%);
324
- --gate-fill: #e8e8ea;
325
- --gate-fill-fg: #151517;
326
- --gate-fill-hover: #c4c4cc;
327
- --gate-error: #f25a5a;
328
- --gate-field: #1c1c1f;
329
- --gate-ring: #c4c4cc;
330
- --gate-ring-dim: rgb(255 255 255 / 12%);
331
- }
332
- *, *::before, *::after { box-sizing: border-box; }
333
- html, body { height: 100%; margin: 0; }
334
- body {
335
- min-height: 100dvh;
336
- font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
337
- color: var(--gate-fg);
338
- background:
339
- radial-gradient(120% 80% at 50% -10%, rgb(255 255 255 / 70%), transparent 55%),
340
- radial-gradient(90% 70% at 50% 120%, rgb(20 22 26 / 4%), transparent 50%),
341
- var(--gate-bg);
342
- -webkit-font-smoothing: antialiased;
343
- -webkit-tap-highlight-color: transparent;
344
- }
345
- html[data-theme="dark"] body {
346
- background:
347
- radial-gradient(120% 80% at 50% -10%, rgb(255 255 255 / 6%), transparent 55%),
348
- radial-gradient(90% 70% at 50% 120%, rgb(0 0 0 / 35%), transparent 50%),
349
- var(--gate-bg);
350
- }
351
- .gate {
352
- min-height: 100dvh;
353
- display: flex;
354
- flex-direction: column;
355
- align-items: center;
356
- justify-content: center;
357
- padding:
358
- max(1.5rem, env(safe-area-inset-top))
359
- max(1.25rem, env(safe-area-inset-right))
360
- max(1.75rem, env(safe-area-inset-bottom))
361
- max(1.25rem, env(safe-area-inset-left));
362
- }
363
- .panel {
364
- width: min(100%, 22.5rem);
365
- display: flex;
366
- flex-direction: column;
367
- align-items: stretch;
368
- gap: 0;
369
- }
370
- .mark {
371
- font-size: 13px;
372
- font-weight: 650;
373
- letter-spacing: 0.14em;
374
- line-height: 1;
375
- color: var(--gate-fg);
376
- margin: 0 0 1.75rem;
377
- text-align: center;
378
- }
379
- h1 {
380
- margin: 0 0 0.5rem;
381
- font-size: clamp(1.375rem, 4.2vw, 1.625rem);
382
- font-weight: 600;
383
- letter-spacing: -0.02em;
384
- line-height: 1.25;
385
- text-align: center;
386
- }
387
- .hint {
388
- margin: 0 0 1.5rem;
389
- font-size: 0.9375rem;
390
- line-height: 1.45;
391
- color: var(--gate-muted);
392
- text-align: center;
393
- text-wrap: pretty;
394
- }
395
- form {
396
- display: flex;
397
- flex-direction: column;
398
- gap: 0.875rem;
399
- }
400
- .field-label {
401
- position: absolute;
402
- width: 1px;
403
- height: 1px;
404
- padding: 0;
405
- margin: -1px;
406
- overflow: hidden;
407
- clip: rect(0, 0, 0, 0);
408
- white-space: nowrap;
409
- border: 0;
410
- }
411
- #access-pin {
412
- width: 100%;
413
- height: 3.25rem;
414
- padding: 0 1rem;
415
- border: 1px solid var(--gate-border);
416
- border-radius: 14px;
417
- background: var(--gate-field);
418
- color: var(--gate-fg);
419
- font-size: 1.375rem;
420
- font-variant-numeric: tabular-nums;
421
- letter-spacing: 0.28em;
422
- text-align: center;
423
- caret-color: var(--gate-fg);
424
- outline: none;
425
- transition: border-color 120ms ease, box-shadow 120ms ease;
426
- }
427
- #access-pin::placeholder {
428
- color: var(--gate-muted);
429
- letter-spacing: 0.12em;
430
- opacity: 0.7;
431
- }
432
- #access-pin:focus-visible {
433
- border-color: var(--gate-fg);
434
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--gate-fg) 18%, transparent);
435
- }
436
- #access-pin[aria-invalid="true"] {
437
- border-color: var(--gate-error);
438
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--gate-error) 22%, transparent);
439
- }
440
- #pin-submit {
441
- display: inline-flex;
442
- align-items: center;
443
- justify-content: center;
444
- width: 100%;
445
- height: 2.75rem;
446
- margin: 0;
447
- padding: 0 1rem;
448
- border: none;
449
- border-radius: 18px;
450
- background: var(--gate-fill);
451
- color: var(--gate-fill-fg);
452
- font: inherit;
453
- font-size: 0.9375rem;
454
- font-weight: 560;
455
- line-height: 1;
456
- cursor: pointer;
457
- transition: background 120ms ease, opacity 120ms ease;
458
- }
459
- #pin-submit:hover:not(:disabled) { background: var(--gate-fill-hover); }
460
- #pin-submit:disabled {
461
- cursor: not-allowed;
462
- opacity: 0.45;
463
- }
464
- #pin-submit:focus-visible {
465
- outline: 2px solid var(--gate-fg);
466
- outline-offset: 2px;
467
- }
468
- #pin-status {
469
- min-height: 1.25rem;
470
- margin: 0;
471
- font-size: 0.875rem;
472
- line-height: 1.35;
473
- text-align: center;
474
- color: var(--gate-muted);
475
- }
476
- #pin-status[data-tone="error"] { color: var(--gate-error); }
477
- #pin-status[data-tone="busy"] { color: var(--gate-muted); }
478
- @media (max-width: 480px) {
479
- #access-pin {
480
- height: 3.5rem;
481
- font-size: 1.5rem;
482
- letter-spacing: 0.32em;
483
- border-radius: 16px;
484
- }
485
- #pin-submit { height: 3rem; font-size: 1rem; }
486
- }
487
- @media (prefers-reduced-motion: reduce) {
488
- #access-pin, #pin-submit { transition: none; }
489
- }
329
+ const GATE_SHELL_CSS = `
330
+ :root {
331
+ color-scheme: light dark;
332
+ --gate-bg: #f4f4f5;
333
+ --gate-fg: #14161a;
334
+ --gate-muted: #5c6168;
335
+ --gate-border: rgb(0 0 0 / 12%);
336
+ --gate-fill: #14161a;
337
+ --gate-fill-fg: #f4f4f5;
338
+ --gate-fill-hover: #2a2d33;
339
+ --gate-error: #c41e1e;
340
+ --gate-field: #ffffff;
341
+ --gate-ring: #5c6168;
342
+ --gate-ring-dim: rgb(0 0 0 / 14%);
343
+ }
344
+ html[data-theme="dark"] {
345
+ color-scheme: dark;
346
+ --gate-bg: #151517;
347
+ --gate-fg: #e8e8ea;
348
+ --gate-muted: #9a9aa3;
349
+ --gate-border: rgb(255 255 255 / 12%);
350
+ --gate-fill: #e8e8ea;
351
+ --gate-fill-fg: #151517;
352
+ --gate-fill-hover: #c4c4cc;
353
+ --gate-error: #f25a5a;
354
+ --gate-field: #1c1c1f;
355
+ --gate-ring: #c4c4cc;
356
+ --gate-ring-dim: rgb(255 255 255 / 12%);
357
+ }
358
+ *, *::before, *::after { box-sizing: border-box; }
359
+ html, body { height: 100%; margin: 0; }
360
+ body {
361
+ min-height: 100dvh;
362
+ font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
363
+ color: var(--gate-fg);
364
+ background:
365
+ radial-gradient(120% 80% at 50% -10%, rgb(255 255 255 / 70%), transparent 55%),
366
+ radial-gradient(90% 70% at 50% 120%, rgb(20 22 26 / 4%), transparent 50%),
367
+ var(--gate-bg);
368
+ -webkit-font-smoothing: antialiased;
369
+ -webkit-tap-highlight-color: transparent;
370
+ }
371
+ html[data-theme="dark"] body {
372
+ background:
373
+ radial-gradient(120% 80% at 50% -10%, rgb(255 255 255 / 6%), transparent 55%),
374
+ radial-gradient(90% 70% at 50% 120%, rgb(0 0 0 / 35%), transparent 50%),
375
+ var(--gate-bg);
376
+ }
377
+ .gate {
378
+ min-height: 100dvh;
379
+ display: flex;
380
+ flex-direction: column;
381
+ align-items: center;
382
+ justify-content: center;
383
+ padding:
384
+ max(1.5rem, env(safe-area-inset-top))
385
+ max(1.25rem, env(safe-area-inset-right))
386
+ max(1.75rem, env(safe-area-inset-bottom))
387
+ max(1.25rem, env(safe-area-inset-left));
388
+ }
389
+ .panel {
390
+ width: min(100%, 22.5rem);
391
+ display: flex;
392
+ flex-direction: column;
393
+ align-items: stretch;
394
+ gap: 0;
395
+ }
396
+ .mark {
397
+ font-size: 13px;
398
+ font-weight: 650;
399
+ letter-spacing: 0.14em;
400
+ line-height: 1;
401
+ color: var(--gate-fg);
402
+ margin: 0 0 1.75rem;
403
+ text-align: center;
404
+ }
405
+ h1 {
406
+ margin: 0 0 0.5rem;
407
+ font-size: clamp(1.375rem, 4.2vw, 1.625rem);
408
+ font-weight: 600;
409
+ letter-spacing: -0.02em;
410
+ line-height: 1.25;
411
+ text-align: center;
412
+ }
413
+ .hint {
414
+ margin: 0 0 1.5rem;
415
+ font-size: 0.9375rem;
416
+ line-height: 1.45;
417
+ color: var(--gate-muted);
418
+ text-align: center;
419
+ text-wrap: pretty;
420
+ }
421
+ form {
422
+ display: flex;
423
+ flex-direction: column;
424
+ gap: 0.875rem;
425
+ }
426
+ .field-label {
427
+ position: absolute;
428
+ width: 1px;
429
+ height: 1px;
430
+ padding: 0;
431
+ margin: -1px;
432
+ overflow: hidden;
433
+ clip: rect(0, 0, 0, 0);
434
+ white-space: nowrap;
435
+ border: 0;
436
+ }
437
+ #access-pin {
438
+ width: 100%;
439
+ height: 3.25rem;
440
+ padding: 0 1rem;
441
+ border: 1px solid var(--gate-border);
442
+ border-radius: 14px;
443
+ background: var(--gate-field);
444
+ color: var(--gate-fg);
445
+ font-size: 1.375rem;
446
+ font-variant-numeric: tabular-nums;
447
+ letter-spacing: 0.28em;
448
+ text-align: center;
449
+ caret-color: var(--gate-fg);
450
+ outline: none;
451
+ transition: border-color 120ms ease, box-shadow 120ms ease;
452
+ }
453
+ #access-pin::placeholder {
454
+ color: var(--gate-muted);
455
+ letter-spacing: 0.12em;
456
+ opacity: 0.7;
457
+ }
458
+ #access-pin:focus-visible {
459
+ border-color: var(--gate-fg);
460
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--gate-fg) 18%, transparent);
461
+ }
462
+ #access-pin[aria-invalid="true"] {
463
+ border-color: var(--gate-error);
464
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--gate-error) 22%, transparent);
465
+ }
466
+ #pin-submit {
467
+ display: inline-flex;
468
+ align-items: center;
469
+ justify-content: center;
470
+ width: 100%;
471
+ height: 2.75rem;
472
+ margin: 0;
473
+ padding: 0 1rem;
474
+ border: none;
475
+ border-radius: 18px;
476
+ background: var(--gate-fill);
477
+ color: var(--gate-fill-fg);
478
+ font: inherit;
479
+ font-size: 0.9375rem;
480
+ font-weight: 560;
481
+ line-height: 1;
482
+ cursor: pointer;
483
+ transition: background 120ms ease, opacity 120ms ease;
484
+ }
485
+ #pin-submit:hover:not(:disabled) { background: var(--gate-fill-hover); }
486
+ #pin-submit:disabled {
487
+ cursor: not-allowed;
488
+ opacity: 0.45;
489
+ }
490
+ #pin-submit:focus-visible {
491
+ outline: 2px solid var(--gate-fg);
492
+ outline-offset: 2px;
493
+ }
494
+ #pin-status {
495
+ min-height: 1.25rem;
496
+ margin: 0;
497
+ font-size: 0.875rem;
498
+ line-height: 1.35;
499
+ text-align: center;
500
+ color: var(--gate-muted);
501
+ }
502
+ #pin-status[data-tone="error"] { color: var(--gate-error); }
503
+ #pin-status[data-tone="busy"] { color: var(--gate-muted); }
504
+ @media (max-width: 480px) {
505
+ #access-pin {
506
+ height: 3.5rem;
507
+ font-size: 1.5rem;
508
+ letter-spacing: 0.32em;
509
+ border-radius: 16px;
510
+ }
511
+ #pin-submit { height: 3rem; font-size: 1rem; }
512
+ }
513
+ @media (prefers-reduced-motion: reduce) {
514
+ #access-pin, #pin-submit { transition: none; }
515
+ }
490
516
  `.replace(/\n/g, "");
491
- const GATE_THEME_BOOT = `(function(){
492
- var dark = typeof matchMedia !== "undefined"
493
- && matchMedia("(prefers-color-scheme: dark)").matches;
494
- document.documentElement.dataset.theme = dark ? "dark" : "light";
495
- document.documentElement.style.colorScheme = dark ? "dark" : "light";
517
+ /**
518
+ * Stamp `data-theme` from durable Settings (same resolve as product
519
+ * `injectBootTheme`), not OS-only `prefers-color-scheme`.
520
+ */
521
+ export function gateThemeBootScript(preference = "system") {
522
+ return `(function(){
523
+ var preference = ${JSON.stringify(preference)};
524
+ var systemDark = preference === "system"
525
+ && typeof matchMedia !== "undefined"
526
+ && matchMedia("(prefers-color-scheme: dark)").matches;
527
+ var dark = preference === "dark" || systemDark;
528
+ document.documentElement.dataset.theme = dark ? "dark" : "light";
529
+ document.documentElement.style.colorScheme = dark ? "dark" : "light";
496
530
  })();`;
497
- function pinPageHtml(mode, title) {
531
+ }
532
+ function pinPageHtml(mode, title, preference = "system") {
498
533
  const kind = mode === "wan" ? "远程" : "局域网";
499
- return `<!doctype html>
500
- <html lang="zh-CN">
501
- <head>
502
- <meta charset="utf-8">
503
- <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover,interactive-widget=resizes-content">
504
- <meta name="color-scheme" content="light dark">
505
- <meta name="theme-color" content="#151517" media="(prefers-color-scheme: dark)">
506
- <meta name="theme-color" content="#f4f4f5" media="(prefers-color-scheme: light)">
507
- <title>${title} · XRK Harness</title>
508
- <link rel="icon" type="image/svg+xml" href="/favicon.svg">
509
- <style>${GATE_SHELL_CSS}</style>
510
- <script>${GATE_THEME_BOOT}</script>
511
- </head>
512
- <body>
513
- <main class="gate">
514
- <div class="panel">
515
- <div class="mark" aria-hidden="true">HARNESS</div>
516
- <h1>${title}</h1>
517
- <p class="hint">输入 Host 启动日志中的 8 位${kind}访问 PIN。</p>
518
- <form id="pin-form" novalidate>
519
- <label class="field-label" for="access-pin">访问 PIN</label>
520
- <input
521
- id="access-pin"
522
- name="pin"
523
- type="text"
524
- inputmode="numeric"
525
- autocomplete="one-time-code"
526
- enterkeyhint="go"
527
- maxlength="8"
528
- pattern="[0-9]{8}"
529
- placeholder="········"
530
- required
531
- autofocus
532
- spellcheck="false"
533
- aria-describedby="pin-status"
534
- >
535
- <button id="pin-submit" type="submit">进入</button>
536
- <output id="pin-status" role="status" aria-live="polite"></output>
537
- </form>
538
- </div>
539
- </main>
540
- <script src="/mobile-access/${mode}-pin.js" defer></script>
541
- </body>
534
+ return `<!doctype html>
535
+ <html lang="zh-CN">
536
+ <head>
537
+ <meta charset="utf-8">
538
+ <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover,interactive-widget=resizes-content">
539
+ <meta name="color-scheme" content="light dark">
540
+ <meta name="theme-color" content="#151517" media="(prefers-color-scheme: dark)">
541
+ <meta name="theme-color" content="#f4f4f5" media="(prefers-color-scheme: light)">
542
+ <title>${title} · XRK Harness</title>
543
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
544
+ <style>${GATE_SHELL_CSS}</style>
545
+ <script>${gateThemeBootScript(preference)}</script>
546
+ </head>
547
+ <body>
548
+ <main class="gate">
549
+ <div class="panel">
550
+ <div class="mark" aria-hidden="true">HARNESS</div>
551
+ <h1>${title}</h1>
552
+ <p class="hint">输入 Host 启动日志中的 8 位${kind}访问 PIN。</p>
553
+ <form id="pin-form" novalidate>
554
+ <label class="field-label" for="access-pin">访问 PIN</label>
555
+ <input
556
+ id="access-pin"
557
+ name="pin"
558
+ type="text"
559
+ inputmode="numeric"
560
+ autocomplete="one-time-code"
561
+ enterkeyhint="go"
562
+ maxlength="8"
563
+ pattern="[0-9]{8}"
564
+ placeholder="········"
565
+ required
566
+ autofocus
567
+ spellcheck="false"
568
+ aria-describedby="pin-status"
569
+ >
570
+ <button id="pin-submit" type="submit">进入</button>
571
+ <output id="pin-status" role="status" aria-live="polite"></output>
572
+ </form>
573
+ </div>
574
+ </main>
575
+ <script src="/mobile-access/${mode}-pin.js" defer></script>
576
+ </body>
542
577
  </html>`;
543
578
  }
579
+ /** Build a LAN/WAN PIN gate document with the active appearance preference. */
580
+ export function buildPinPageHtml(mode, preference = "system") {
581
+ return pinPageHtml(mode, mode === "wan" ? "远程访问" : "局域网访问", preference);
582
+ }
544
583
  function pinPageJs(mode) {
545
- return `(() => {
546
- const status = document.getElementById('pin-status')
547
- const input = document.getElementById('access-pin')
548
- const submit = document.getElementById('pin-submit')
549
- const form = document.getElementById('pin-form')
550
- let busy = false
551
-
552
- const setStatus = (text, tone) => {
553
- if (!status) return
554
- status.textContent = text
555
- if (tone) status.dataset.tone = tone
556
- else delete status.dataset.tone
557
- }
558
-
559
- const setBusy = (next) => {
560
- busy = next
561
- if (submit) submit.disabled = next
562
- if (input) input.readOnly = next
563
- }
564
-
565
- input?.addEventListener('input', () => {
566
- const digits = (input.value || '').replace(/\\D/g, '').slice(0, 8)
567
- if (input.value !== digits) input.value = digits
568
- input.removeAttribute('aria-invalid')
569
- if (status?.dataset.tone === 'error') setStatus('')
570
- })
571
-
572
- form?.addEventListener('submit', async (event) => {
573
- event.preventDefault()
574
- if (busy) return
575
- const pin = (input?.value || '').trim()
576
- if (!/^\\d{8}$/.test(pin)) {
577
- input?.setAttribute('aria-invalid', 'true')
578
- setStatus('请输入 8 位数字 PIN', 'error')
579
- input?.focus()
580
- input?.select()
581
- return
582
- }
583
- setBusy(true)
584
- setStatus('验证中…', 'busy')
585
- try {
586
- const response = await fetch('/mobile-access/auth/${mode}-pin', {
587
- method: 'POST',
588
- credentials: 'same-origin',
589
- headers: { 'content-type': 'application/json' },
590
- body: JSON.stringify({ pin }),
591
- })
592
- if (!response.ok) {
593
- input?.setAttribute('aria-invalid', 'true')
594
- setStatus('PIN 不正确,请核对启动日志后重试', 'error')
595
- setBusy(false)
596
- input?.focus()
597
- input?.select()
598
- return
599
- }
600
- try { sessionStorage.setItem('xrk.operatorAccess', '1') } catch {}
601
- const next = new URLSearchParams(location.search).get('return') || '/'
602
- location.replace(next)
603
- } catch {
604
- setStatus('无法连接 Host,请检查网络后重试', 'error')
605
- setBusy(false)
606
- }
607
- })
608
-
609
- queue?.focus()
610
- })()
584
+ return `(() => {
585
+ const status = document.getElementById('pin-status')
586
+ const input = document.getElementById('access-pin')
587
+ const submit = document.getElementById('pin-submit')
588
+ const form = document.getElementById('pin-form')
589
+ let busy = false
590
+
591
+ const setStatus = (text, tone) => {
592
+ if (!status) return
593
+ status.textContent = text
594
+ if (tone) status.dataset.tone = tone
595
+ else delete status.dataset.tone
596
+ }
597
+
598
+ const setBusy = (next) => {
599
+ busy = next
600
+ if (submit) submit.disabled = next
601
+ if (input) input.readOnly = next
602
+ }
603
+
604
+ input?.addEventListener('input', () => {
605
+ const digits = (input.value || '').replace(/\\D/g, '').slice(0, 8)
606
+ if (input.value !== digits) input.value = digits
607
+ input.removeAttribute('aria-invalid')
608
+ if (status?.dataset.tone === 'error') setStatus('')
609
+ })
610
+
611
+ form?.addEventListener('submit', async (event) => {
612
+ event.preventDefault()
613
+ if (busy) return
614
+ const pin = (input?.value || '').trim()
615
+ if (!/^\\d{8}$/.test(pin)) {
616
+ input?.setAttribute('aria-invalid', 'true')
617
+ setStatus('请输入 8 位数字 PIN', 'error')
618
+ input?.focus()
619
+ input?.select()
620
+ return
621
+ }
622
+ setBusy(true)
623
+ setStatus('验证中…', 'busy')
624
+ try {
625
+ const response = await fetch('/mobile-access/auth/${mode}-pin', {
626
+ method: 'POST',
627
+ credentials: 'same-origin',
628
+ headers: { 'content-type': 'application/json' },
629
+ body: JSON.stringify({ pin }),
630
+ })
631
+ if (!response.ok) {
632
+ input?.setAttribute('aria-invalid', 'true')
633
+ setStatus('PIN 不正确,请核对启动日志后重试', 'error')
634
+ setBusy(false)
635
+ input?.focus()
636
+ input?.select()
637
+ return
638
+ }
639
+ try { sessionStorage.setItem('xrk.operatorAccess', '1') } catch {}
640
+ const next = new URLSearchParams(location.search).get('return') || '/'
641
+ location.replace(next)
642
+ } catch {
643
+ setStatus('无法连接 Host,请检查网络后重试', 'error')
644
+ setBusy(false)
645
+ }
646
+ })
647
+
648
+ queue?.focus()
649
+ })()
611
650
  `;
612
651
  }
613
- export const LAN_PIN_PAGE_HTML = pinPageHtml("lan", "局域网访问");
614
- export const WAN_PIN_PAGE_HTML = pinPageHtml("wan", "远程访问");
652
+ /** Default PIN HTML (`system` → OS scheme). Prefer {@link buildPinPageHtml}. */
653
+ export const LAN_PIN_PAGE_HTML = buildPinPageHtml("lan", "system");
654
+ export const WAN_PIN_PAGE_HTML = buildPinPageHtml("wan", "system");
615
655
  export const LAN_PIN_PAGE_JS = pinPageJs("lan");
616
656
  export const WAN_PIN_PAGE_JS = pinPageJs("wan");
617
657
  //# sourceMappingURL=mobile-access-auth.js.map