@goodandready/dsh-github-ops 0.2.3 → 0.2.4
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 +5 -0
- package/lib/auth.js +64 -0
- package/lib/index.js +11 -30
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@goodandready/dsh-github-ops`.
|
|
4
4
|
|
|
5
|
+
## 0.2.4
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Fixed GitHub client cache holding onto stale credentials when rotated with a token of the exact same length: cache key now incorporates a non-reversible sha256 token fingerprint (#38).
|
|
9
|
+
|
|
5
10
|
## 0.2.3
|
|
6
11
|
|
|
7
12
|
### Added
|
package/lib/auth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
1
2
|
// Access resolution across the sources a GitHub user actually has.
|
|
2
3
|
//
|
|
3
4
|
// Order (tokenSource: "auto"): the DSH credentials service, then the environment variable of
|
|
@@ -69,3 +70,66 @@ export async function resolveAccess({ credentials, tokenEnv, tokenSource = 'auto
|
|
|
69
70
|
}
|
|
70
71
|
return { value: '', source: '', guidance: NO_ACCESS_GUIDANCE }
|
|
71
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Deterministic non-reversible token fingerprint for client cache keys:
|
|
75
|
+
* differentiates tokens of identical length without leaking token content into logs or keys.
|
|
76
|
+
*/
|
|
77
|
+
export function tokenFingerprint(value) {
|
|
78
|
+
if (!value || typeof value !== 'string') return ''
|
|
79
|
+
return createHash('sha256').update(value).digest('hex').slice(0, 16)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Cache key for shared GitHub client instances.
|
|
84
|
+
* Incorporates the token fingerprint (not length) so rotation between equal-length tokens
|
|
85
|
+
* creates a new client and uses the new Bearer immediately.
|
|
86
|
+
*/
|
|
87
|
+
export function clientCacheKey(access, cfg = {}) {
|
|
88
|
+
return [
|
|
89
|
+
access?.source || '',
|
|
90
|
+
tokenFingerprint(access?.value),
|
|
91
|
+
cfg?.baseUrl || '',
|
|
92
|
+
cfg?.timeoutMs || '',
|
|
93
|
+
cfg?.cacheTtlMs || '',
|
|
94
|
+
cfg?.maxRetries ?? '',
|
|
95
|
+
].join('|')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Manages the shared GitHub client instance:
|
|
100
|
+
* caches the client while the key matches; recreates it when the token or config changes.
|
|
101
|
+
*/
|
|
102
|
+
export function createClientHolder({ createClient, signalProvider } = {}) {
|
|
103
|
+
let sharedClient = null
|
|
104
|
+
let sharedKey = ''
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
get(access, cfg = {}) {
|
|
108
|
+
if (!access || !access.value) {
|
|
109
|
+
throw new Error(`GitHub access is not configured: ${access?.guidance || NO_ACCESS_GUIDANCE}`)
|
|
110
|
+
}
|
|
111
|
+
const key = clientCacheKey(access, cfg)
|
|
112
|
+
if (!sharedClient || sharedKey !== key) {
|
|
113
|
+
if (typeof createClient !== 'function') {
|
|
114
|
+
throw new Error('createClient factory must be a function')
|
|
115
|
+
}
|
|
116
|
+
sharedClient = createClient({
|
|
117
|
+
token: access.value,
|
|
118
|
+
baseUrl: cfg.baseUrl || 'https://api.github.com',
|
|
119
|
+
timeoutMs: Number(cfg.timeoutMs) || 30000,
|
|
120
|
+
cacheTtlMs: Number.isFinite(Number(cfg.cacheTtlMs)) ? Number(cfg.cacheTtlMs) : 60000,
|
|
121
|
+
maxRetries: Number.isFinite(Number(cfg.maxRetries)) ? Number(cfg.maxRetries) : 2,
|
|
122
|
+
signalProvider,
|
|
123
|
+
})
|
|
124
|
+
sharedKey = key
|
|
125
|
+
}
|
|
126
|
+
return sharedClient
|
|
127
|
+
},
|
|
128
|
+
currentKey: () => sharedKey,
|
|
129
|
+
currentClient: () => sharedClient,
|
|
130
|
+
reset: () => {
|
|
131
|
+
sharedClient = null
|
|
132
|
+
sharedKey = ''
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
15
15
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
16
16
|
|
|
17
17
|
import { createGitHubClient, parseRepoSpec, describeError, GitHubError } from './github.js'
|
|
18
|
-
import { resolveAccess, NO_ACCESS_GUIDANCE } from './auth.js'
|
|
18
|
+
import { resolveAccess, NO_ACCESS_GUIDANCE, createClientHolder } from './auth.js'
|
|
19
19
|
import { resolveRepoContext } from './repo-context.js'
|
|
20
20
|
import { presentationFor } from './present.js'
|
|
21
21
|
import { createRecords, startReviewJob } from './jobs.js'
|
|
@@ -233,39 +233,20 @@ export function apply(ctx, rawConfig) {
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
// One client per configuration: the read cache lives in it, and the signal for the
|
|
236
|
-
// current tool call arrives through the async context.
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
// current tool call arrives through the async context. Token rotation immediately recreates
|
|
237
|
+
// the client even when the new token has the exact same length.
|
|
238
|
+
const clientHolder = createClientHolder({
|
|
239
|
+
createClient: createGitHubClient,
|
|
240
|
+
signalProvider: () => {
|
|
241
|
+
const store = callContext.getStore()
|
|
242
|
+
return store && store.signal ? store.signal : undefined
|
|
243
|
+
},
|
|
244
|
+
})
|
|
239
245
|
|
|
240
246
|
async function client() {
|
|
241
247
|
const cfg = liveConfig()
|
|
242
248
|
const access = await resolveToken()
|
|
243
|
-
|
|
244
|
-
throw new Error(`GitHub access is not configured: ${access.guidance || NO_ACCESS_GUIDANCE}`)
|
|
245
|
-
}
|
|
246
|
-
const key = [
|
|
247
|
-
access.source,
|
|
248
|
-
access.value.length,
|
|
249
|
-
cfg.baseUrl || '',
|
|
250
|
-
cfg.timeoutMs || '',
|
|
251
|
-
cfg.cacheTtlMs || '',
|
|
252
|
-
cfg.maxRetries ?? '',
|
|
253
|
-
].join('|')
|
|
254
|
-
if (!sharedClient || sharedKey !== key) {
|
|
255
|
-
sharedClient = createGitHubClient({
|
|
256
|
-
token: access.value,
|
|
257
|
-
baseUrl: cfg.baseUrl || 'https://api.github.com',
|
|
258
|
-
timeoutMs: Number(cfg.timeoutMs) || 30000,
|
|
259
|
-
cacheTtlMs: Number.isFinite(Number(cfg.cacheTtlMs)) ? Number(cfg.cacheTtlMs) : 60000,
|
|
260
|
-
maxRetries: Number.isFinite(Number(cfg.maxRetries)) ? Number(cfg.maxRetries) : 2,
|
|
261
|
-
signalProvider: () => {
|
|
262
|
-
const store = callContext.getStore()
|
|
263
|
-
return store && store.signal ? store.signal : undefined
|
|
264
|
-
},
|
|
265
|
-
})
|
|
266
|
-
sharedKey = key
|
|
267
|
-
}
|
|
268
|
-
return sharedClient
|
|
249
|
+
return clientHolder.get(access, cfg)
|
|
269
250
|
}
|
|
270
251
|
|
|
271
252
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-github-ops",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "GitHub operations for DeepSeek Harness: releases, tags, a guarded generic API pass-through, workflow runs, secrets and sanitized mirror publication.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|