opencode-harness-kit 0.3.8 → 0.3.9

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 CHANGED
@@ -14,6 +14,25 @@ is not happening.
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.3.9] — 2026-10-20
18
+
19
+ ### Fixed
20
+
21
+ - **Each npm command gets its own two-factor challenge.** The code a browser
22
+ flow returns is valid for the challenge that asked for it and for nothing
23
+ else, so carrying one from one command to the next made the second command
24
+ send a code that was already spent - and the registry answered that with a
25
+ different challenge, one that wants an authenticator code rather than a
26
+ browser. The failure looked like "npm wants something else now" and nothing
27
+ like the cause.
28
+ - **The URLs come from the response body, not from a guess.** The done URL
29
+ was rebuilt from the id, which sent a real request to the registry with a
30
+ made-up id; the registry answered "forbidden", which is not a token, so the
31
+ wait ran its full length and then reported that nobody had answered.
32
+ - **`OC_TRUST_NO_BROWSER` turns the browser off.** A test that opens a
33
+ browser on the machine it runs on is a test that breaks whatever the
34
+ person at the keyboard was doing.
35
+
17
36
  ## [0.3.8] — 2026-10-20
18
37
 
19
38
  ### Fixed
@@ -358,7 +377,8 @@ silent in the worst way:
358
377
 
359
378
  ---
360
379
 
361
- [Unreleased]: https://github.com/pham-canh/space-harness/compare/v0.3.8...HEAD
380
+ [Unreleased]: https://github.com/pham-canh/space-harness/compare/v0.3.9...HEAD
381
+ [0.3.9]: https://github.com/pham-canh/space-harness/compare/v0.3.8...v0.3.9
362
382
  [0.3.8]: https://github.com/pham-canh/space-harness/compare/v0.3.7...v0.3.8
363
383
  [0.3.7]: https://github.com/pham-canh/space-harness/compare/v0.3.6...v0.3.7
364
384
  [0.3.6]: https://github.com/pham-canh/space-harness/compare/v0.3.5...v0.3.6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-harness-kit",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "Harness kit for OpenCode: capability profiles, a fleet of job lanes with one free model each, role-based reasoning effort, context pruning, and measured baselines.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/pham-canh/space-harness#readme",
package/src/trust.js CHANGED
@@ -87,11 +87,10 @@ function npm(args, opts = {}) {
87
87
  * Run npm with its output captured.
88
88
  *
89
89
  * @param {string[]} args
90
- * @param {{ otp?: string }} [state]
90
+ * @param {string[]} [extra] arguments to add, such as `--otp=<code>`
91
91
  * @returns {{ out: string, code: number }}
92
92
  */
93
- function npmCaptured(args, state = {}) {
94
- const extra = state.otp ? [`--otp=${state.otp}`] : []
93
+ function npmCaptured(args, extra = []) {
95
94
  const r = spawnSync("npm", [...args, ...extra], {
96
95
  encoding: "utf8",
97
96
  stdio: ["ignore", "pipe", "pipe"],
@@ -117,26 +116,44 @@ function npmCaptured(args, state = {}) {
117
116
  * it, and npm then gives up and prints a URL nobody can open. So the flow is
118
117
  * done here, where the URL can be recovered.
119
118
  *
119
+ * EACH CALL GETS ITS OWN CHALLENGE. The code a browser flow returns is valid
120
+ * for the challenge that asked for it and for nothing else, so carrying one
121
+ * from one npm command to the next makes the second command send a code that
122
+ * is already spent, and the registry answers that with a different challenge
123
+ * - one that wants an authenticator code rather than a browser.
124
+ *
120
125
  * @param {string[]} args
121
- * @param {{ otp?: string }} [state]
126
+ * @param {{ otp?: string }} [state] the user's own code, if they gave one
122
127
  * @returns {Promise<{ out: string, code: number }>}
123
128
  */
124
129
  async function npmWithOtp(args, state = {}) {
125
- let result = npmCaptured(args, state)
130
+ const userOtp = state.otp ? [`--otp=${state.otp}`] : []
131
+ let result = npmCaptured(args, userOtp)
126
132
  if (result.code === 0) return result
127
133
 
128
- const m = /"doneUrl"\s*:\s*"[^"]*authId=([a-f0-9-]+)"/.exec(result.out)
129
- if (!m) return result
130
- const authId = m[1]
131
- const authUrl = `https://www.npmjs.com/auth/cli/${authId}`
132
- const doneUrl = `https://registry.npmjs.org/-/v1/done?authId=${authId}`
134
+ // The URLs come FROM the body, not from a guess. Guessing the done URL
135
+ // sends a real request to the registry with a made-up id, and the registry
136
+ // answers "forbidden" - which is not a token, so the wait runs its full
137
+ // length and then reports that nobody answered.
138
+ const done = /"doneUrl"\s*:\s*"([^"]+)"/.exec(result.out)
139
+ if (!done) return result
140
+ const doneUrl = done[1]
141
+ const auth = /"authUrl"\s*:\s*"([^"]+)"/.exec(result.out)
142
+ const authId = /authId=([a-f0-9-]+)/.exec(doneUrl)
143
+ const authUrl = auth ? auth[1] : `https://www.npmjs.com/auth/cli/${authId?.[1] ?? ""}`
133
144
 
134
145
  // Open it if this machine can. The URL is printed either way, because a
135
146
  // browser that cannot be opened here can be opened anywhere.
136
- try {
137
- execFileSync("xdg-open", [authUrl], { stdio: "ignore", timeout: 10_000 })
138
- } catch {
139
- // No browser here. The URL below is the way out.
147
+ //
148
+ // OC_TRUST_NO_BROWSER turns the opening off, which is what a test needs:
149
+ // a test that opens a browser on the machine it runs on is a test that
150
+ // breaks whatever the person at the keyboard was doing.
151
+ if (!process.env.OC_TRUST_NO_BROWSER) {
152
+ try {
153
+ execFileSync("xdg-open", [authUrl], { stdio: "ignore", timeout: 10_000 })
154
+ } catch {
155
+ // No browser here. The URL below is the way out.
156
+ }
140
157
  }
141
158
  console.error("")
142
159
  console.error(bold(" npm needs to confirm it is you."))
@@ -151,8 +168,8 @@ async function npmWithOtp(args, state = {}) {
151
168
  console.error(red(" no answer came back, so nothing was changed."))
152
169
  return result
153
170
  }
154
- state.otp = token
155
- result = npmCaptured(args, state)
171
+ // The code is used for THIS call only.
172
+ result = npmCaptured(args, [`--otp=${token}`])
156
173
  return result
157
174
  }
158
175