ssos-user-cli 0.2.1 → 0.2.2

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/auth.mjs +92 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssos-user-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Official SSOS command-line client for secure workspace access and AI-assisted business tools",
5
5
  "type": "module",
6
6
  "repository": {
package/src/auth.mjs CHANGED
@@ -55,6 +55,75 @@ function defaultKeychain() {
55
55
  return import(keychainModule).then(module => module.default ?? module)
56
56
  }
57
57
 
58
+ function escapeHtml(value) {
59
+ return String(value).replace(/[&<>"']/g, character => ({
60
+ '&': '&amp;',
61
+ '<': '&lt;',
62
+ '>': '&gt;',
63
+ '"': '&quot;',
64
+ "'": '&#39;',
65
+ })[character])
66
+ }
67
+
68
+ export function renderAuthorizationCallbackPage({ status = 'success', title, message, detail } = {}) {
69
+ const success = status === 'success'
70
+ const safeTitle = escapeHtml(title ?? (success ? '授权已完成' : '授权未完成'))
71
+ const safeMessage = escapeHtml(message ?? (success ? '创业OS CLI 已完成安全授权。' : '本次授权没有完成。'))
72
+ const safeDetail = detail ? escapeHtml(detail) : ''
73
+ const accent = success ? '#22c55e' : '#ef4444'
74
+ const softAccent = success ? '#dcfce7' : '#fee2e2'
75
+ const icon = success ? '✓' : '!'
76
+ return `<!doctype html>
77
+ <html lang="zh-CN">
78
+ <head>
79
+ <meta charset="utf-8">
80
+ <meta name="viewport" content="width=device-width, initial-scale=1">
81
+ <meta name="color-scheme" content="light dark">
82
+ <title>${safeTitle} · 创业OS</title>
83
+ <style>
84
+ :root { color-scheme: light dark; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
85
+ * { box-sizing: border-box; }
86
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px; color: #172033; background: linear-gradient(135deg, #f7f9fc 0%, #eef3fb 100%); }
87
+ .shell { width: min(100%, 520px); }
88
+ .brand { display: flex; align-items: center; gap: 10px; margin: 0 0 18px 4px; font-weight: 700; letter-spacing: -.02em; }
89
+ .brand-mark { width: 34px; height: 34px; display: grid; place-items: center; border-radius: 10px; color: white; background: #2563eb; box-shadow: 0 8px 24px rgba(37, 99, 235, .25); }
90
+ .brand-name { font-size: 17px; }
91
+ .card { overflow: hidden; border: 1px solid rgba(148, 163, 184, .28); border-radius: 24px; background: rgba(255, 255, 255, .9); box-shadow: 0 24px 70px rgba(30, 64, 175, .12); backdrop-filter: blur(16px); }
92
+ .topline { height: 5px; background: ${accent}; }
93
+ .content { padding: 42px 36px 36px; text-align: center; }
94
+ .icon { width: 68px; height: 68px; display: grid; place-items: center; margin: 0 auto 22px; border-radius: 22px; color: ${accent}; background: ${softAccent}; font-size: 34px; font-weight: 700; }
95
+ h1 { margin: 0; font-size: 26px; line-height: 1.25; letter-spacing: -.03em; }
96
+ .message { margin: 14px auto 0; max-width: 370px; color: #526079; font-size: 15px; line-height: 1.7; }
97
+ .detail { margin: 22px 0 0; padding: 13px 15px; border-radius: 14px; color: #64748b; background: #f8fafc; font-size: 13px; line-height: 1.6; word-break: break-word; }
98
+ .hint { margin: 26px 0 0; padding-top: 20px; border-top: 1px solid #e2e8f0; color: #7a879b; font-size: 12px; line-height: 1.6; }
99
+ @media (prefers-color-scheme: dark) {
100
+ body { color: #e5e7eb; background: linear-gradient(135deg, #101827 0%, #172554 100%); }
101
+ .card { border-color: rgba(148, 163, 184, .24); background: rgba(15, 23, 42, .88); box-shadow: 0 24px 70px rgba(2, 6, 23, .4); }
102
+ .message { color: #a6b3c7; }
103
+ .detail { color: #a6b3c7; background: rgba(30, 41, 59, .75); }
104
+ .hint { border-color: #334155; color: #94a3b8; }
105
+ }
106
+ @media (max-width: 480px) { body { padding: 16px; } .content { padding: 34px 22px 26px; } h1 { font-size: 23px; } }
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <main class="shell" aria-live="polite">
111
+ <div class="brand"><span class="brand-mark" aria-hidden="true">OS</span><span class="brand-name">创业OS</span></div>
112
+ <section class="card">
113
+ <div class="topline" aria-hidden="true"></div>
114
+ <div class="content">
115
+ <div class="icon" aria-hidden="true">${icon}</div>
116
+ <h1>${safeTitle}</h1>
117
+ <p class="message">${safeMessage}</p>
118
+ ${safeDetail ? `<p class="detail">${safeDetail}</p>` : ''}
119
+ <p class="hint">${success ? '可以返回终端继续使用。此页面可以安全关闭。' : '请返回终端重新运行 ssos login。'}</p>
120
+ </div>
121
+ </section>
122
+ </main>
123
+ </body>
124
+ </html>`
125
+ }
126
+
58
127
  export function createUserCliAuth(options = {}) {
59
128
  const env = options.env ?? process.env
60
129
  const config = createUserCliConfig(env)
@@ -118,9 +187,30 @@ export function createUserCliAuth(options = {}) {
118
187
  const error = url.searchParams.get('error')
119
188
  const codeValue = url.searchParams.get('code')
120
189
  if (error || !codeValue || url.searchParams.get('state') !== state) {
121
- response.writeHead(400); response.end('OAuth authorization failed'); server.close(); reject(new Error(error ?? 'OAuth state or code is invalid')); return
190
+ response.writeHead(400, {
191
+ 'Content-Type': 'text/html; charset=utf-8',
192
+ 'Cache-Control': 'no-store',
193
+ 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'",
194
+ })
195
+ response.end(renderAuthorizationCallbackPage({
196
+ status: 'error',
197
+ title: '授权未完成',
198
+ message: '创业OS 没有收到有效的授权回调。',
199
+ detail: error ?? '授权状态或一次性授权码无效',
200
+ }))
201
+ server.close(); reject(new Error(error ?? 'OAuth state or code is invalid')); return
122
202
  }
123
- response.writeHead(200); response.end('SSOS authorization complete; you may close this window.'); server.close(); resolve(codeValue)
203
+ response.writeHead(200, {
204
+ 'Content-Type': 'text/html; charset=utf-8',
205
+ 'Cache-Control': 'no-store',
206
+ 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'",
207
+ })
208
+ response.end(renderAuthorizationCallbackPage({
209
+ title: '授权已完成',
210
+ message: '创业OS CLI 已完成安全授权。',
211
+ detail: '工作区权限已确认,短期访问令牌只会保存在当前 CLI 进程中。',
212
+ }))
213
+ server.close(); resolve(codeValue)
124
214
  })
125
215
  server.listen(config.redirectPort, '127.0.0.1')
126
216
  const timeout = setTimeout(() => { server.close(); reject(new Error('OAuth authorization timed out')) }, 5 * 60 * 1000)