@boostkit-dsh/auth-management 0.1.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 - 2026-09-22
4
+
5
+ - 首次发布前将包名统一为 `@boostkit-dsh/auth-management`,去掉 scope 下重复的 `plugin-` 前缀。
6
+ - 将 DSH 精确兼容基线升级为 `0.1.7-alpha.1`。
7
+ - Client 产物改用 DSH `__ModuleLoader__` 包装,避免合并脚本中的 ESM `export` 使全部 Web Client Module 加载失败。
8
+
9
+ ## 0.1.0 - 2026-09-22
10
+
11
+ - 增加 GitCode PAT 写入前验证、当前 `ActorRef`、登出和失效凭据清理。
12
+ - 增加本地 JSONL 最小化审计、筛选接口和鉴权/审计管理页面。
13
+ - 除健康检查、登录、凭据替换和登出外,所有接口要求有效 GitCode 身份。
package/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # 鉴权与审计管理
2
+
3
+ `@boostkit-dsh/auth-management` 是鲲鹏 Demo 所有用户可见能力的身份前置插件。用户必须填写 GitCode PAT;插件通过 `GET https://api.gitcode.com/api/v5/user` 和 `Authorization: Bearer` 请求头验证后,才提供 `provider: gitcode` 的当前 `ActorRef`。
4
+
5
+ PAT 由 DSH `ctx.credentials` 保存到 `auth-management/gitcode` 记录,默认落在 `$DSH_HOME/.credentials.yaml`(通常是 `~/.dsh/.credentials.yaml`)。它不会进入项目 `.dsh/`、HTTP 状态响应、审计、日志或诊断信息。该文件是同一 OS 用户可读的明文凭据存储,不是 Keychain,也不是同用户 Agent 的隔离边界。
6
+
7
+ ## MVP 能力
8
+
9
+ - 登录、换号、状态检查和登出;
10
+ - 每个受保护操作重新验证 GitCode 身份,401/403 后清除失效记录;
11
+ - `ctx.authManagement.requireActor()` 统一返回当前 GitCode `ActorRef` 或抛出 `AUTH_REQUIRED`;
12
+ - `ctx.authManagement.executeAudited()` 对变更动作写入开始与最终结果事件;
13
+ - 本地 JSONL 审计查询,支持 action、outcome、actor、时间和 limit 筛选;
14
+ - 独立登录与审计页面,未鉴权业务请求会引导回该页面。
15
+
16
+ 默认审计文件是 `$DSH_HOME/audit/kunpeng-demo.jsonl`。事件只包含身份、动作、目标、结果、时间、工作区、Session 和可选证据引用,不包含 PAT、完整提示词、模型原始输出或源码正文。
17
+
18
+ 真实 PAT、失效凭据、现有插件门禁和热重载按[手工验收说明](../../docs/auth-management-manual-test.md)执行。
19
+
20
+ ## 未包含
21
+
22
+ 首版不包含 OAuth、SSO、RBAC、组织同步、中央审计后台、提示词/源码采集、自动评分、导出和删除界面。真实 GitCode、Windows 权限行为和浏览器交互仍需按项目手工验收执行。
package/audit.js ADDED
@@ -0,0 +1,108 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { appendFile, chmod, mkdir, readFile } from 'node:fs/promises'
3
+ import { dirname } from 'node:path'
4
+
5
+ const MAX_TEXT = 500
6
+ const MAX_AUDIT_BYTES = 16 * 1024 * 1024
7
+ const OUTCOMES = new Set(['started', 'succeeded', 'failed', 'cancelled', 'rejected'])
8
+
9
+ function limited(value, field, { optional = false } = {}) {
10
+ if (value === undefined && optional) return undefined
11
+ if (typeof value !== 'string' || value === '' || value.length > MAX_TEXT || /[\r\n]/.test(value)) throw new TypeError(`AuditEvent.${field} 非法`)
12
+ return value
13
+ }
14
+
15
+ function normalizedActor(actor) {
16
+ if (actor?.provider !== 'gitcode') throw new TypeError('AuditEvent.actor 必须是 GitCode ActorRef')
17
+ return {
18
+ provider: 'gitcode',
19
+ id: limited(String(actor.id ?? ''), 'actor.id'),
20
+ login: limited(actor.login, 'actor.login'),
21
+ displayName: limited(actor.displayName, 'actor.displayName'),
22
+ }
23
+ }
24
+
25
+ function normalizedTarget(target) {
26
+ return {
27
+ type: limited(target?.type, 'target.type'),
28
+ id: limited(target?.id, 'target.id'),
29
+ ...(target?.label === undefined ? {} : { label: limited(target.label, 'target.label') }),
30
+ }
31
+ }
32
+
33
+ export function normalizeAuditEvent(value, { now = () => new Date(), id = randomUUID } = {}) {
34
+ const outcome = limited(value?.outcome, 'outcome')
35
+ if (!OUTCOMES.has(outcome)) throw new TypeError('AuditEvent.outcome 非法')
36
+ return {
37
+ schema: 1,
38
+ id: id(),
39
+ timestamp: now().toISOString(),
40
+ actor: normalizedActor(value.actor),
41
+ action: limited(value.action, 'action'),
42
+ target: normalizedTarget(value.target),
43
+ outcome,
44
+ ...(value.workspaceId === undefined ? {} : { workspaceId: limited(value.workspaceId, 'workspaceId') }),
45
+ ...(value.sessionId === undefined ? {} : { sessionId: limited(value.sessionId, 'sessionId') }),
46
+ ...(value.evidenceRef === undefined ? {} : { evidenceRef: limited(value.evidenceRef, 'evidenceRef') }),
47
+ ...(value.summary === undefined ? {} : { summary: limited(value.summary, 'summary') }),
48
+ }
49
+ }
50
+
51
+ function normalizeFilter(filter = {}) {
52
+ const limit = Number.isInteger(filter.limit) ? Math.min(Math.max(filter.limit, 1), 500) : 100
53
+ return {
54
+ limit,
55
+ action: typeof filter.action === 'string' && filter.action !== '' ? filter.action : undefined,
56
+ outcome: typeof filter.outcome === 'string' && filter.outcome !== '' ? filter.outcome : undefined,
57
+ actorId: typeof filter.actorId === 'string' && filter.actorId !== '' ? filter.actorId : undefined,
58
+ since: typeof filter.since === 'string' && !Number.isNaN(Date.parse(filter.since)) ? filter.since : undefined,
59
+ until: typeof filter.until === 'string' && !Number.isNaN(Date.parse(filter.until)) ? filter.until : undefined,
60
+ }
61
+ }
62
+
63
+ export function createJsonlAuditStore(options) {
64
+ if (typeof options?.path !== 'string' || options.path === '') throw new TypeError('audit path 必填')
65
+ const path = options.path
66
+ const now = options.now ?? (() => new Date())
67
+ const id = options.id ?? randomUUID
68
+ let disposed = false
69
+ let tail = Promise.resolve()
70
+
71
+ async function append(value) {
72
+ if (disposed) throw new Error('audit store 已释放')
73
+ const event = normalizeAuditEvent(value, { now, id })
74
+ const write = tail.then(async () => {
75
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 })
76
+ await appendFile(path, `${JSON.stringify(event)}\n`, { encoding: 'utf8', mode: 0o600 })
77
+ if (process.platform !== 'win32') await chmod(path, 0o600)
78
+ })
79
+ tail = write.catch(() => {})
80
+ await write
81
+ return event
82
+ }
83
+
84
+ async function query(filterValue = {}) {
85
+ if (disposed) throw new Error('audit store 已释放')
86
+ await tail
87
+ const filter = normalizeFilter(filterValue)
88
+ let contents
89
+ try { contents = await readFile(path) } catch (error) {
90
+ if (error?.code === 'ENOENT') return []
91
+ throw error
92
+ }
93
+ if (contents.length > MAX_AUDIT_BYTES) throw new Error('审计文件超过 Demo 查询上限')
94
+ const events = contents.toString('utf8').split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line))
95
+ return events.filter((event) => {
96
+ if (filter.action !== undefined && event.action !== filter.action) return false
97
+ if (filter.outcome !== undefined && event.outcome !== filter.outcome) return false
98
+ if (filter.actorId !== undefined && event.actor?.id !== filter.actorId) return false
99
+ if (filter.since !== undefined && event.timestamp < filter.since) return false
100
+ if (filter.until !== undefined && event.timestamp > filter.until) return false
101
+ return true
102
+ }).slice(-filter.limit).reverse()
103
+ }
104
+
105
+ return { path, append, query, dispose() { disposed = true } }
106
+ }
107
+
108
+ export const __testing = { normalizeFilter }
package/auth.js ADDED
@@ -0,0 +1,273 @@
1
+ const DEFAULT_API_BASE = 'https://api.gitcode.com/api/v5'
2
+ const DEFAULT_TIMEOUT_MS = 12_000
3
+ const TOKEN_MAX_BYTES = 4096
4
+
5
+ export const CREDENTIAL_KEY = 'auth-management/gitcode'
6
+ export const AUTH_ERROR_CODES = Object.freeze({
7
+ required: 'AUTH_REQUIRED',
8
+ unavailable: 'AUTH_UNAVAILABLE',
9
+ disposed: 'AUTH_DISPOSED',
10
+ audit: 'AUDIT_UNAVAILABLE',
11
+ })
12
+
13
+ export class AuthManagementError extends Error {
14
+ constructor(code, message, options = {}) {
15
+ super(message, options)
16
+ this.name = 'AuthManagementError'
17
+ this.code = code
18
+ }
19
+ }
20
+
21
+ function authError(code, message) {
22
+ return new AuthManagementError(code, message)
23
+ }
24
+
25
+ function normalizedToken(value) {
26
+ if (typeof value !== 'string') throw authError(AUTH_ERROR_CODES.required, '请填写 GitCode key。')
27
+ const token = value.trim()
28
+ if (token === '' || Buffer.byteLength(token, 'utf8') > TOKEN_MAX_BYTES || /[\r\n]/.test(token)) {
29
+ throw authError(AUTH_ERROR_CODES.required, 'GitCode key 格式无效。')
30
+ }
31
+ return token
32
+ }
33
+
34
+ function actorFromUser(user, verifiedAt) {
35
+ const id = user?.id
36
+ const login = user?.username ?? user?.login
37
+ if ((typeof id !== 'string' && typeof id !== 'number') || typeof login !== 'string' || login === '') {
38
+ throw authError(AUTH_ERROR_CODES.unavailable, 'GitCode 返回了无法识别的用户信息。')
39
+ }
40
+ const displayName = typeof user.name === 'string' && user.name !== '' ? user.name : login
41
+ const actor = {
42
+ provider: 'gitcode',
43
+ id: String(id),
44
+ login,
45
+ displayName,
46
+ verifiedAt,
47
+ }
48
+ if (typeof user.avatar_url === 'string' && user.avatar_url !== '') actor.avatarUrl = user.avatar_url
49
+ return Object.freeze(actor)
50
+ }
51
+
52
+ function storedCredential(record) {
53
+ const payload = record?.kind === 'grant' ? record.payload : undefined
54
+ if (payload?.schema !== 1 || payload.provider !== 'gitcode' || typeof payload.token !== 'string') return null
55
+ return { token: payload.token, actor: payload.actor }
56
+ }
57
+
58
+ function credentialRecord(token, actor) {
59
+ return {
60
+ kind: 'grant',
61
+ payload: { schema: 1, provider: 'gitcode', token, actor },
62
+ }
63
+ }
64
+
65
+ /** Validate one PAT without placing it in a URL, error, result or log message. */
66
+ export async function verifyGitCodeToken(tokenValue, options = {}) {
67
+ const token = normalizedToken(tokenValue)
68
+ const fetchImpl = options.fetchImpl ?? fetch
69
+ const apiBase = String(options.apiBase ?? DEFAULT_API_BASE).replace(/\/+$/, '')
70
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
71
+ const controller = new AbortController()
72
+ const onAbort = () => controller.abort(options.signal?.reason)
73
+ options.signal?.addEventListener('abort', onAbort, { once: true })
74
+ const timer = setTimeout(() => controller.abort(), timeoutMs)
75
+ let response
76
+ try {
77
+ response = await fetchImpl(`${apiBase}/user`, {
78
+ method: 'GET',
79
+ headers: { accept: 'application/json', authorization: `Bearer ${token}` },
80
+ signal: controller.signal,
81
+ })
82
+ } catch {
83
+ throw authError(AUTH_ERROR_CODES.unavailable, '暂时无法验证 GitCode 身份,请检查网络后重试。')
84
+ } finally {
85
+ clearTimeout(timer)
86
+ options.signal?.removeEventListener('abort', onAbort)
87
+ }
88
+ if (response.status === 401 || response.status === 403) {
89
+ throw authError(AUTH_ERROR_CODES.required, 'GitCode key 无效或已失效,请重新填写。')
90
+ }
91
+ if (!response.ok) throw authError(AUTH_ERROR_CODES.unavailable, `GitCode 身份验证暂不可用(HTTP ${response.status})。`)
92
+ let user
93
+ try { user = await response.json() } catch { throw authError(AUTH_ERROR_CODES.unavailable, 'GitCode 返回了无法解析的用户信息。') }
94
+ return actorFromUser(user, (options.now ?? (() => new Date()))().toISOString())
95
+ }
96
+
97
+ /** Create the current-actor and audit facade over DSH credentials. */
98
+ export function createAuthManagementService(options) {
99
+ const credentials = options?.credentials
100
+ const auditStore = options?.auditStore
101
+ if (credentials === undefined || typeof credentials.readRecord !== 'function' || typeof credentials.modifyRecord !== 'function') {
102
+ throw new TypeError('auth-management 需要 DSH credentials 服务')
103
+ }
104
+ if (auditStore === undefined || typeof auditStore.append !== 'function') throw new TypeError('auth-management 需要 auditStore')
105
+ const controllers = new Set()
106
+ let disposed = false
107
+ let mutationTail = Promise.resolve()
108
+
109
+ function ensureLive() {
110
+ if (disposed) throw authError(AUTH_ERROR_CODES.disposed, '鉴权服务已释放。')
111
+ }
112
+
113
+ function serialized(task) {
114
+ const next = mutationTail.then(task, task)
115
+ mutationTail = next.catch(() => {})
116
+ return next
117
+ }
118
+
119
+ async function verify(token) {
120
+ ensureLive()
121
+ const controller = new AbortController()
122
+ controllers.add(controller)
123
+ try {
124
+ return await verifyGitCodeToken(token, {
125
+ fetchImpl: options.fetchImpl,
126
+ apiBase: options.apiBase,
127
+ timeoutMs: options.timeoutMs,
128
+ now: options.now,
129
+ signal: controller.signal,
130
+ })
131
+ } finally {
132
+ controllers.delete(controller)
133
+ }
134
+ }
135
+
136
+ async function readStored() {
137
+ ensureLive()
138
+ return storedCredential(await credentials.readRecord(CREDENTIAL_KEY))
139
+ }
140
+
141
+ async function invalidate(stored, reason) {
142
+ const latest = await readStored()
143
+ if (latest?.token !== stored?.token) return
144
+ await credentials.deleteRecord(CREDENTIAL_KEY)
145
+ if (stored?.actor !== undefined) {
146
+ await auditStore.append({
147
+ actor: stored.actor,
148
+ action: 'auth.invalidate',
149
+ target: { type: 'credential', id: 'gitcode' },
150
+ outcome: 'succeeded',
151
+ summary: reason,
152
+ }).catch(() => {})
153
+ }
154
+ }
155
+
156
+ async function requireActorUnlocked(context = {}) {
157
+ const stored = await readStored()
158
+ if (stored === null) throw authError(AUTH_ERROR_CODES.required, '请先填写有效的 GitCode key。')
159
+ try {
160
+ const actor = await verify(stored.token)
161
+ if (actor.id !== String(stored.actor?.id ?? '') || actor.login !== stored.actor?.login) {
162
+ await credentials.modifyRecord(CREDENTIAL_KEY, (current) => {
163
+ const latest = storedCredential(current)
164
+ return latest?.token === stored.token ? Promise.resolve(credentialRecord(stored.token, actor)) : Promise.resolve(undefined)
165
+ })
166
+ }
167
+ return actor
168
+ } catch (error) {
169
+ if (error?.code === AUTH_ERROR_CODES.required) await invalidate(stored, 'GitCode key 已失效')
170
+ throw error
171
+ }
172
+ }
173
+
174
+ function requireActor(context = {}) {
175
+ return serialized(() => requireActorUnlocked(context))
176
+ }
177
+
178
+ async function login(tokenValue, context = {}) {
179
+ const token = normalizedToken(tokenValue)
180
+ return serialized(async () => {
181
+ ensureLive()
182
+ const actor = await verify(token)
183
+ const previous = await credentials.readRecord(CREDENTIAL_KEY)
184
+ const event = {
185
+ actor,
186
+ action: 'auth.login',
187
+ target: { type: 'account', id: actor.id, label: actor.login },
188
+ workspaceId: context.workspaceId,
189
+ sessionId: context.sessionId,
190
+ }
191
+ await auditStore.append({ ...event, outcome: 'started' })
192
+ await credentials.modifyRecord(CREDENTIAL_KEY, () => Promise.resolve(credentialRecord(token, actor)))
193
+ try {
194
+ await auditStore.append({ ...event, outcome: 'succeeded' })
195
+ } catch {
196
+ if (previous === undefined) await credentials.deleteRecord(CREDENTIAL_KEY)
197
+ else await credentials.modifyRecord(CREDENTIAL_KEY, () => Promise.resolve(previous))
198
+ throw authError(AUTH_ERROR_CODES.audit, '审计不可用,GitCode 登录没有生效。')
199
+ }
200
+ return actor
201
+ })
202
+ }
203
+
204
+ async function logout(context = {}) {
205
+ return serialized(async () => {
206
+ ensureLive()
207
+ const stored = await readStored()
208
+ await credentials.deleteRecord(CREDENTIAL_KEY)
209
+ if (stored?.actor !== undefined) {
210
+ await auditStore.append({
211
+ actor: stored.actor,
212
+ action: 'auth.logout',
213
+ target: { type: 'account', id: String(stored.actor.id), label: stored.actor.login },
214
+ outcome: 'succeeded',
215
+ workspaceId: context.workspaceId,
216
+ sessionId: context.sessionId,
217
+ })
218
+ }
219
+ return { authenticated: false }
220
+ })
221
+ }
222
+
223
+ async function status() {
224
+ try { return { authenticated: true, actor: await requireActor() } } catch (error) {
225
+ if (error?.code === AUTH_ERROR_CODES.required) return { authenticated: false }
226
+ throw error
227
+ }
228
+ }
229
+
230
+ async function executeAudited(event, task) {
231
+ const actor = await requireActor(event)
232
+ const base = { ...event, actor }
233
+ await auditStore.append({ ...base, outcome: 'started' })
234
+ try {
235
+ const result = await task(actor)
236
+ const outcome = typeof event.outcome === 'function' ? event.outcome(result) : 'succeeded'
237
+ await auditStore.append({ ...base, outcome })
238
+ return result
239
+ } catch (error) {
240
+ await auditStore.append({ ...base, outcome: 'failed', summary: safeFailureSummary(error) }).catch(() => {})
241
+ throw error
242
+ }
243
+ }
244
+
245
+ return {
246
+ login,
247
+ logout,
248
+ status,
249
+ requireActor,
250
+ executeAudited,
251
+ queryAudit: (filter) => requireActor().then(() => auditStore.query(filter)),
252
+ dispose() {
253
+ if (disposed) return
254
+ disposed = true
255
+ for (const controller of controllers) controller.abort()
256
+ controllers.clear()
257
+ auditStore.dispose?.()
258
+ },
259
+ }
260
+ }
261
+
262
+ function safeFailureSummary(error) {
263
+ const code = typeof error?.code === 'string' && /^[A-Z0-9_-]{1,80}$/.test(error.code) ? error.code : 'OPERATION_FAILED'
264
+ return code
265
+ }
266
+
267
+ export function publicAuthError(error) {
268
+ const code = Object.values(AUTH_ERROR_CODES).includes(error?.code) ? error.code : 'INTERNAL_ERROR'
269
+ const message = code === 'INTERNAL_ERROR' ? '操作失败,请查看脱敏后的主机日志。' : String(error.message)
270
+ return { code, message }
271
+ }
272
+
273
+ export const __testing = { actorFromUser, credentialRecord, normalizedToken, storedCredential }
package/client.js ADDED
@@ -0,0 +1,197 @@
1
+ // Generated from src/client.ts by scripts/build-clients.mjs. Do not edit.
2
+ window.__ModuleLoader__.load({ id: "@boostkit-dsh/auth-management", factory: (require2) => {
3
+ const module = { exports: {} };
4
+ const exports = module.exports;
5
+ const { createElement: h, useCallback, useEffect, useMemo, useState } = require2("react");
6
+ const NS = "auth-management";
7
+ const PANEL_ID = "auth-management";
8
+ const AUTH_REQUIRED_EVENT = "kunpeng-auth-required";
9
+ const dictionaries = {
10
+ zh: {
11
+ nav: "\u9274\u6743\u4E0E\u5BA1\u8BA1",
12
+ title: "GitCode \u9274\u6743\u4E0E\u5BA1\u8BA1",
13
+ subtitle: "Demo \u7684\u4E1A\u52A1\u9875\u9762\u3001\u8BFB\u53D6\u63A5\u53E3\u3001\u5DE5\u5177\u548C\u5199\u64CD\u4F5C\u5747\u4E0D\u5141\u8BB8\u533F\u540D\u8BBF\u95EE\u3002",
14
+ key: "GitCode key",
15
+ keyHint: "\u4EC5\u5199\u5165 DSH credentials\uFF0C\u4E0D\u4F1A\u8FDB\u5165\u9879\u76EE\u3001\u5BA1\u8BA1\u6216\u65E5\u5FD7\u3002",
16
+ login: "\u9A8C\u8BC1\u5E76\u767B\u5F55",
17
+ loggingIn: "\u6B63\u5728\u9A8C\u8BC1\u2026",
18
+ authenticated: "\u5DF2\u9274\u6743",
19
+ unauthenticated: "\u672A\u9274\u6743",
20
+ logout: "\u9000\u51FA\u5E76\u6E05\u9664\u51ED\u636E",
21
+ refresh: "\u5237\u65B0",
22
+ account: "GitCode \u8D26\u53F7",
23
+ storage: "\u51ED\u636E\u9ED8\u8BA4\u4FDD\u5B58\u5728 $DSH_HOME/.credentials.yaml\uFF1B\u8FD9\u662F OS \u7528\u6237\u6743\u9650\u4FDD\u62A4\u7684\u660E\u6587\u6587\u4EF6\uFF0C\u4E0D\u662F Keychain\u3002",
24
+ audit: "\u6700\u8FD1\u5BA1\u8BA1",
25
+ noAudit: "\u6682\u65E0\u5BA1\u8BA1\u4E8B\u4EF6",
26
+ action: "\u52A8\u4F5C",
27
+ target: "\u76EE\u6807",
28
+ outcome: "\u7ED3\u679C",
29
+ time: "\u65F6\u95F4",
30
+ actor: "\u64CD\u4F5C\u8005",
31
+ failed: "\u64CD\u4F5C\u5931\u8D25",
32
+ required: "\u8BF7\u586B\u5199\u6709\u6548 GitCode key \u540E\u7EE7\u7EED\u3002",
33
+ auditHelp: "\u9ED8\u8BA4\u4E0D\u8BB0\u5F55 PAT\u3001\u5B8C\u6574\u63D0\u793A\u8BCD\u3001\u6A21\u578B\u539F\u59CB\u8F93\u51FA\u6216\u6E90\u7801\u6B63\u6587\u3002"
34
+ },
35
+ en: {
36
+ nav: "Auth & Audit",
37
+ title: "GitCode Authentication & Audit",
38
+ subtitle: "Anonymous access to Demo pages, reads, tools, and writes is disabled.",
39
+ key: "GitCode key",
40
+ keyHint: "Stored only in DSH credentials; never in the project, audit, or logs.",
41
+ login: "Verify and sign in",
42
+ loggingIn: "Verifying\u2026",
43
+ authenticated: "Authenticated",
44
+ unauthenticated: "Authentication required",
45
+ logout: "Sign out and remove credential",
46
+ refresh: "Refresh",
47
+ account: "GitCode account",
48
+ storage: "Credentials default to $DSH_HOME/.credentials.yaml, an OS-user-protected plaintext file rather than a keychain.",
49
+ audit: "Recent audit",
50
+ noAudit: "No audit events yet",
51
+ action: "Action",
52
+ target: "Target",
53
+ outcome: "Outcome",
54
+ time: "Time",
55
+ actor: "Actor",
56
+ failed: "Operation failed",
57
+ required: "Enter a valid GitCode key to continue.",
58
+ auditHelp: "PATs, full prompts, raw model output, and source bodies are not recorded by default."
59
+ }
60
+ };
61
+ const styles = {
62
+ page: { flex: 1, minHeight: 0, overflow: "auto", boxSizing: "border-box", padding: "24px clamp(18px,4vw,48px)", background: "var(--dsw-alias-bg-base)" },
63
+ content: { width: "100%", maxWidth: 980, margin: "0 auto", display: "flex", flexDirection: "column", gap: 14 },
64
+ row: { display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" },
65
+ title: { margin: 0, fontSize: 20, color: "var(--dsw-alias-label-primary)" },
66
+ muted: { color: "var(--dsw-alias-label-tertiary)", fontSize: 12, lineHeight: 1.6 },
67
+ card: { padding: 16, border: "1px solid var(--dsw-alias-border-l1)", borderRadius: 10, background: "var(--dsw-alias-bg-layer-1)", display: "flex", flexDirection: "column", gap: 10 },
68
+ input: { minWidth: 300, flex: 1, padding: "8px 10px", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: 7, color: "var(--dsw-alias-label-primary)", background: "var(--dsw-alias-bg-base)", font: "inherit" },
69
+ button: { padding: "7px 12px", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: 7, color: "var(--dsw-alias-label-primary)", background: "var(--dsw-alias-bg-layer-2)", cursor: "pointer", font: "inherit" },
70
+ primary: { borderColor: "var(--dsw-alias-state-business-primary)", color: "var(--dsw-alias-state-business-primary)" },
71
+ status: { padding: "2px 8px", borderRadius: 999, border: "1px solid var(--dsw-alias-border-l2)", fontSize: 12 },
72
+ error: { padding: 10, borderRadius: 8, border: "1px solid var(--dsw-alias-state-error-primary)", color: "var(--dsw-alias-state-error-primary)", fontSize: 12 },
73
+ table: { width: "100%", borderCollapse: "collapse", fontSize: 12 },
74
+ cell: { padding: "7px 8px", borderBottom: "1px solid var(--dsw-alias-border-l1)", textAlign: "left", verticalAlign: "top", wordBreak: "break-word" }
75
+ };
76
+ async function responseJson(response) {
77
+ const value = await response.json();
78
+ if (!response.ok || value?.ok === false) throw Object.assign(new Error(value?.error?.message ?? value?.error ?? `HTTP ${response.status}`), { code: value?.error?.code });
79
+ return value;
80
+ }
81
+ function AuthIcon({ size = 16, active = false }) {
82
+ const color = active ? "var(--dsw-alias-state-business-primary)" : "currentColor";
83
+ return h(
84
+ "svg",
85
+ { width: size, height: size, viewBox: "0 0 24 24", fill: "none", "aria-hidden": true },
86
+ h("path", { d: "M7 10V7a5 5 0 0 1 10 0v3m-11 0h12v10H6V10Zm6 4v2", stroke: color, strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round" })
87
+ );
88
+ }
89
+ function AuthPage({ t }) {
90
+ const [state, setState] = useState(null);
91
+ const [events, setEvents] = useState([]);
92
+ const [key, setKey] = useState("");
93
+ const [busy, setBusy] = useState(false);
94
+ const [error, setError] = useState(null);
95
+ const load = useCallback(async () => {
96
+ setError(null);
97
+ try {
98
+ const next = await fetch("/kunpeng-auth/status", { cache: "no-store" }).then(responseJson);
99
+ setState(next);
100
+ if (next.authenticated) {
101
+ const audit = await fetch("/kunpeng-auth/audit?limit=100", { cache: "no-store" }).then(responseJson);
102
+ setEvents(audit.events ?? []);
103
+ } else setEvents([]);
104
+ } catch (err) {
105
+ setState(null);
106
+ setEvents([]);
107
+ setError(err?.message ?? String(err));
108
+ }
109
+ }, []);
110
+ useEffect(() => {
111
+ void load();
112
+ }, [load]);
113
+ const login = useCallback(async () => {
114
+ setBusy(true);
115
+ setError(null);
116
+ try {
117
+ await fetch("/kunpeng-auth/login", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ key }) }).then(responseJson);
118
+ setKey("");
119
+ await load();
120
+ } catch (err) {
121
+ setError(err?.message ?? String(err));
122
+ } finally {
123
+ setBusy(false);
124
+ }
125
+ }, [key, load]);
126
+ const logout = useCallback(async () => {
127
+ setBusy(true);
128
+ setError(null);
129
+ try {
130
+ await fetch("/kunpeng-auth/logout", { method: "POST" }).then(responseJson);
131
+ await load();
132
+ } catch (err) {
133
+ setError(err?.message ?? String(err));
134
+ } finally {
135
+ setBusy(false);
136
+ }
137
+ }, [load]);
138
+ const actorLabel = useMemo(() => state?.actor ? `${state.actor.displayName} (@${state.actor.login})` : "", [state]);
139
+ return h("main", { style: styles.page }, h(
140
+ "div",
141
+ { style: styles.content },
142
+ h("div", null, h("h2", { style: styles.title }, t("title")), h("div", { style: styles.muted }, t("subtitle"))),
143
+ h(
144
+ "section",
145
+ { style: styles.card },
146
+ h("div", { style: styles.row }, h("span", { style: styles.status }, state?.authenticated ? t("authenticated") : t("unauthenticated")), state?.authenticated ? h("strong", null, `${t("account")}\uFF1A${actorLabel}`) : null),
147
+ state?.authenticated ? h("div", { style: styles.row }, h("button", { type: "button", style: styles.button, disabled: busy, onClick: () => void logout() }, t("logout")), h("button", { type: "button", style: styles.button, disabled: busy, onClick: () => void load() }, t("refresh"))) : h(
148
+ "form",
149
+ { style: styles.row, onSubmit: (event) => {
150
+ event.preventDefault();
151
+ void login();
152
+ } },
153
+ h("input", { type: "password", autoComplete: "off", value: key, disabled: busy, placeholder: t("key"), style: styles.input, onChange: (event) => setKey(event.target.value) }),
154
+ h("button", { type: "submit", disabled: busy || key.trim() === "", style: { ...styles.button, ...styles.primary } }, busy ? t("loggingIn") : t("login"))
155
+ ),
156
+ h("div", { style: styles.muted }, state?.authenticated ? t("storage") : `${t("required")} ${t("keyHint")}`),
157
+ error ? h("div", { style: styles.error }, `${t("failed")}\uFF1A${error}`) : null
158
+ ),
159
+ h(
160
+ "section",
161
+ { style: styles.card },
162
+ h("div", { style: styles.row }, h("strong", null, t("audit")), h("span", { style: styles.muted }, t("auditHelp"))),
163
+ events.length === 0 ? h("div", { style: styles.muted }, t("noAudit")) : h("div", { style: { overflowX: "auto" } }, h(
164
+ "table",
165
+ { style: styles.table },
166
+ h("thead", null, h("tr", null, ...["time", "actor", "action", "target", "outcome"].map((name) => h("th", { key: name, style: styles.cell }, t(name))))),
167
+ h("tbody", null, ...events.map((event) => h(
168
+ "tr",
169
+ { key: event.id },
170
+ h("td", { style: styles.cell }, new Date(event.timestamp).toLocaleString()),
171
+ h("td", { style: styles.cell }, `@${event.actor.login}`),
172
+ h("td", { style: styles.cell }, event.action),
173
+ h("td", { style: styles.cell }, `${event.target.type}:${event.target.label ?? event.target.id}`),
174
+ h("td", { style: styles.cell }, event.outcome)
175
+ )))
176
+ ))
177
+ )
178
+ ));
179
+ }
180
+ function apply(ctx) {
181
+ ctx.effect(() => ctx.locale.register(NS, dictionaries), "auth-management: dictionaries");
182
+ const t = ctx.locale.bind(NS);
183
+ ctx.slots.inject("main", () => ctx.slots.register({ name: "main", key: PANEL_ID, locale: NS, inject: () => ({ t }) }, AuthPage));
184
+ ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({ name: "sidebar.panellist", id: PANEL_ID, order: 4, label: () => t("nav"), locale: NS }, AuthIcon));
185
+ const requireLogin = () => ctx.layout.selectPanel(PANEL_ID);
186
+ ctx.provide("authManagementClient", { openLogin: requireLogin });
187
+ window.addEventListener(AUTH_REQUIRED_EVENT, requireLogin);
188
+ ctx.effect(() => () => window.removeEventListener(AUTH_REQUIRED_EVENT, requireLogin), "auth-management: auth-required-listener");
189
+ void fetch("/kunpeng-auth/status", { cache: "no-store" }).then(responseJson).then((state) => {
190
+ if (!state.authenticated) requireLogin();
191
+ }).catch(requireLogin);
192
+ }
193
+ exports.name = "auth-management-client";
194
+ exports.inject = ["slots", "locale", "layout"];
195
+ exports.apply = apply;
196
+ return module.exports;
197
+ } });
@@ -0,0 +1,4 @@
1
+ # 鉴权与审计管理:GitCode 当前操作者、本地审计和登录页面。
2
+ - insert:
3
+ - id: auth-management
4
+ name: '@boostkit-dsh/auth-management'
package/http.js ADDED
@@ -0,0 +1,56 @@
1
+ import { AUTH_ERROR_CODES, publicAuthError } from './auth.js'
2
+
3
+ const REQUEST_BYTES = 8192
4
+
5
+ export function sameOrigin(request) {
6
+ const origin = request.headers.origin
7
+ const host = request.headers.host
8
+ if (origin === undefined || host === undefined) return false
9
+ const remote = request.socket?.remoteAddress
10
+ const loopback = remote === '::1' || remote === '127.0.0.1' || (typeof remote === 'string' && remote.startsWith('127.')) || (typeof remote === 'string' && remote.startsWith('::ffff:127.'))
11
+ if (!loopback) return false
12
+ try { return new URL(origin).host === host } catch { return false }
13
+ }
14
+
15
+ export function sendJson(response, status, payload) {
16
+ if (response.destroyed || response.writableEnded) return
17
+ response.writeHead(status, { 'cache-control': 'no-store', 'content-type': 'application/json; charset=utf-8' })
18
+ response.end(JSON.stringify(payload))
19
+ }
20
+
21
+ export async function readJsonBody(request) {
22
+ const chunks = []
23
+ let size = 0
24
+ for await (const chunk of request) {
25
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
26
+ size += buffer.length
27
+ if (size > REQUEST_BYTES) throw new Error('request body too large')
28
+ chunks.push(buffer)
29
+ }
30
+ const text = Buffer.concat(chunks).toString('utf8').trim()
31
+ return text === '' ? {} : JSON.parse(text)
32
+ }
33
+
34
+ export function sendError(response, error) {
35
+ const body = publicAuthError(error)
36
+ const status = body.code === AUTH_ERROR_CODES.required ? 401 : body.code === AUTH_ERROR_CODES.unavailable ? 503 : 500
37
+ sendJson(response, status, { ok: false, error: body })
38
+ }
39
+
40
+ function stringQuery(url, name) {
41
+ const value = url.searchParams.get(name)
42
+ return typeof value === 'string' && value.length > 0 && value.length <= 500 ? value : undefined
43
+ }
44
+
45
+ export function auditFilter(url) {
46
+ const limitText = url.searchParams.get('limit')
47
+ const limit = limitText === null ? undefined : Number(limitText)
48
+ return {
49
+ action: stringQuery(url, 'action'),
50
+ outcome: stringQuery(url, 'outcome'),
51
+ actorId: stringQuery(url, 'actorId'),
52
+ since: stringQuery(url, 'since'),
53
+ until: stringQuery(url, 'until'),
54
+ limit: Number.isInteger(limit) ? limit : undefined,
55
+ }
56
+ }
package/index.js ADDED
@@ -0,0 +1,90 @@
1
+ import { homedir } from 'node:os'
2
+ import { join, resolve } from 'node:path'
3
+
4
+ import { createJsonlAuditStore } from './audit.js'
5
+ import { createAuthManagementService } from './auth.js'
6
+ import { auditFilter, readJsonBody, sameOrigin, sendError, sendJson } from './http.js'
7
+
8
+ export const name = 'auth-management'
9
+ export const inject = ['credentials']
10
+
11
+ const API_PREFIX = '/kunpeng-auth'
12
+
13
+ function defaultAuditPath() {
14
+ const dshHome = typeof process.env.DSH_HOME === 'string' && process.env.DSH_HOME !== '' ? process.env.DSH_HOME : join(homedir(), '.dsh')
15
+ return join(dshHome, 'audit', 'kunpeng-demo.jsonl')
16
+ }
17
+
18
+ function method(request, response, expected) {
19
+ if (request.method === expected) return true
20
+ response.writeHead(405, { allow: expected })
21
+ response.end()
22
+ return false
23
+ }
24
+
25
+ /** Register GitCode authentication, current ActorRef, local audit and HTTP/UI endpoints. */
26
+ export function apply(ctx, config = {}) {
27
+ const credentials = ctx.get?.('credentials') ?? ctx.credentials
28
+ const auditStore = createJsonlAuditStore({
29
+ path: config.auditPath === undefined ? defaultAuditPath() : resolve(config.auditPath),
30
+ now: config.now,
31
+ id: config.auditId,
32
+ })
33
+ const service = createAuthManagementService({
34
+ credentials,
35
+ auditStore,
36
+ fetchImpl: config.fetchImpl,
37
+ apiBase: config.apiBase,
38
+ timeoutMs: config.timeoutMs,
39
+ now: config.now,
40
+ })
41
+ ctx.provide('authManagement', service)
42
+ ctx.effect?.(() => () => service.dispose(), 'auth-management.dispose')
43
+
44
+ ctx.inject(['webServer'], (host) => {
45
+ host.effect(() => {
46
+ const routes = [
47
+ {
48
+ kind: 'exact', path: `${API_PREFIX}/health`, handler: (request, response) => {
49
+ if (!method(request, response, 'GET')) return
50
+ sendJson(response, 200, { ok: true, service: 'auth-management' })
51
+ },
52
+ },
53
+ {
54
+ kind: 'exact', path: `${API_PREFIX}/status`, handler: (request, response) => {
55
+ if (!method(request, response, 'GET')) return
56
+ void service.status().then((value) => sendJson(response, 200, { ok: true, ...value })).catch((error) => sendError(response, error))
57
+ },
58
+ },
59
+ {
60
+ kind: 'exact', path: `${API_PREFIX}/login`, handler: (request, response) => {
61
+ if (!method(request, response, 'POST')) return
62
+ if (!sameOrigin(request)) return sendJson(response, 403, { ok: false, error: { code: 'ORIGIN_REQUIRED', message: '只允许 DSH 本机页面写入凭据。' } })
63
+ void readJsonBody(request).then((body) => service.login(body.key, { workspaceId: body.workspaceId, sessionId: body.sessionId })).then((actor) => sendJson(response, 200, { ok: true, authenticated: true, actor })).catch((error) => sendError(response, error))
64
+ },
65
+ },
66
+ {
67
+ kind: 'exact', path: `${API_PREFIX}/logout`, handler: (request, response) => {
68
+ if (!method(request, response, 'POST')) return
69
+ if (!sameOrigin(request)) return sendJson(response, 403, { ok: false, error: { code: 'ORIGIN_REQUIRED', message: '只允许 DSH 本机页面清除凭据。' } })
70
+ void service.logout().then((value) => sendJson(response, 200, { ok: true, ...value })).catch((error) => sendError(response, error))
71
+ },
72
+ },
73
+ {
74
+ kind: 'exact', path: `${API_PREFIX}/audit`, handler: (request, response) => {
75
+ if (!method(request, response, 'GET')) return
76
+ let filter
77
+ try { filter = auditFilter(new URL(request.url ?? '', 'http://localhost')) } catch { return sendJson(response, 400, { ok: false, error: { code: 'INVALID_REQUEST', message: '查询参数无效。' } }) }
78
+ void service.queryAudit(filter).then((events) => sendJson(response, 200, { ok: true, events })).catch((error) => sendError(response, error))
79
+ },
80
+ },
81
+ ]
82
+ const disposers = routes.map((route) => host.webServer.register(route))
83
+ return () => { for (const dispose of disposers) dispose() }
84
+ }, 'auth-management.http')
85
+ })
86
+ }
87
+
88
+ export { createJsonlAuditStore, normalizeAuditEvent } from './audit.js'
89
+ export { AUTH_ERROR_CODES, AuthManagementError, createAuthManagementService, publicAuthError, verifyGitCodeToken } from './auth.js'
90
+ export const __testing = { defaultAuditPath, sameOrigin }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@boostkit-dsh/auth-management",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "description": "GitCode 强制鉴权、当前操作者与本地最小化使用审计管理",
6
+ "main": "index.js",
7
+ "files": [
8
+ "CHANGELOG.md",
9
+ "README.md",
10
+ "audit.js",
11
+ "auth.js",
12
+ "client.js",
13
+ "cordis.patch.yml",
14
+ "http.js",
15
+ "index.js"
16
+ ],
17
+ "license": "UNLICENSED",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://gitcode.com/lujinxing/kunpeng-dsh-demo.git",
21
+ "directory": "plugins/auth-management"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public",
25
+ "registry": "https://registry.npmjs.org"
26
+ },
27
+ "scripts": {
28
+ "test": "node --test test/*.test.mjs"
29
+ },
30
+ "dsh": {
31
+ "bundle": {
32
+ "patch": "./cordis.patch.yml"
33
+ },
34
+ "client": {
35
+ "platform": "web",
36
+ "inject": [
37
+ "@deepseek-ai/dsh-client-locale",
38
+ "@deepseek-ai/dsh-client-ui-layout",
39
+ "@deepseek-ai/dsh-client-ui-sidebar"
40
+ ]
41
+ },
42
+ "market": {
43
+ "displayName": {
44
+ "zh": "鉴权与审计管理",
45
+ "en": "Authentication & Audit"
46
+ },
47
+ "category": "kunpeng-rd",
48
+ "maintainers": [
49
+ "@lujinxing"
50
+ ],
51
+ "compatibility": {
52
+ "dsh": "0.1.7-alpha.1"
53
+ },
54
+ "allowedBuilds": []
55
+ }
56
+ },
57
+ "exports": {
58
+ ".": "./index.js",
59
+ "./client": "./client.js",
60
+ "./package.json": "./package.json"
61
+ }
62
+ }