@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 +13 -0
- package/README.md +22 -0
- package/audit.js +108 -0
- package/auth.js +273 -0
- package/client.js +197 -0
- package/cordis.patch.yml +4 -0
- package/http.js +56 -0
- package/index.js +90 -0
- package/package.json +62 -0
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
|
+
} });
|
package/cordis.patch.yml
ADDED
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
|
+
}
|