@lpm-registry/cli 0.7.0 → 0.9.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.
- package/CHANGELOG.md +10 -0
- package/lib/api.js +59 -4
- package/lib/commands/login.js +53 -5
- package/lib/commands/npmrc.js +3 -2
- package/lib/commands/setup.js +3 -2
- package/lib/config.js +20 -0
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.8.0] - 2026-04-01
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **Rust binary v0.8.0** — Install hash fast-exit (82ms → 26ms up-to-date), NDJSON streaming batch metadata, resolver carries tarball URLs (eliminates double metadata fetch), up-to-date benchmark added.
|
|
10
|
+
- **Scoped registry default** — `lpm setup` and `lpm npmrc` now write scoped config (`@lpm.dev:registry=`) by default. npm packages go direct to npmjs.org (faster, $0 proxy cost). Use `--proxy` to opt into full proxy mode (Pro/Org feature).
|
|
11
|
+
- **Global config defaults** — `lpm config set allowNew true`, `lpm config set autoBuild true`, etc. CLI flags merge with `~/.lpm/config.toml` defaults.
|
|
12
|
+
- **Migrate fix** — Corrected stale registry URL in `lpm migrate` (.npmrc now writes `/api/registry/` instead of `/api/packages/`).
|
|
13
|
+
- Version bump to stay in sync with Rust binary releases.
|
|
14
|
+
|
|
5
15
|
## [0.7.0] - 2026-04-01
|
|
6
16
|
|
|
7
17
|
### Changed
|
package/lib/api.js
CHANGED
|
@@ -9,7 +9,15 @@
|
|
|
9
9
|
* @module cli/lib/api
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
getRefreshToken,
|
|
14
|
+
getRegistryUrl,
|
|
15
|
+
getRetries,
|
|
16
|
+
getTimeout,
|
|
17
|
+
getToken,
|
|
18
|
+
setRefreshToken,
|
|
19
|
+
setToken,
|
|
20
|
+
} from "./config.js"
|
|
13
21
|
import {
|
|
14
22
|
ERROR_MESSAGES,
|
|
15
23
|
RATE_LIMIT_STATUS_CODES,
|
|
@@ -63,6 +71,44 @@ function parseRetryAfter(retryAfter) {
|
|
|
63
71
|
return RETRY_BASE_DELAY_MS
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Attempt silent token refresh using the stored refresh token (Feature 44 Part B).
|
|
76
|
+
* @returns {Promise<string|null>} New access token if successful, null otherwise
|
|
77
|
+
*/
|
|
78
|
+
async function tryRefresh() {
|
|
79
|
+
try {
|
|
80
|
+
const refreshToken = await getRefreshToken()
|
|
81
|
+
if (!refreshToken) return null
|
|
82
|
+
|
|
83
|
+
const registryUrl = getRegistryUrl()
|
|
84
|
+
const { createHash } = await import("node:crypto")
|
|
85
|
+
const { hostname, userInfo } = await import("node:os")
|
|
86
|
+
const fp = createHash("sha256")
|
|
87
|
+
.update(`${hostname()}:${userInfo().username}:lpm-cli`)
|
|
88
|
+
.digest("hex")
|
|
89
|
+
|
|
90
|
+
const resp = await fetch(`${registryUrl}/api/cli/refresh`, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
body: JSON.stringify({ refreshToken, deviceFingerprint: fp }),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
if (!resp.ok) return null
|
|
97
|
+
|
|
98
|
+
const data = await resp.json()
|
|
99
|
+
if (data.token) {
|
|
100
|
+
await setToken(data.token)
|
|
101
|
+
if (data.refreshToken) {
|
|
102
|
+
await setRefreshToken(data.refreshToken)
|
|
103
|
+
}
|
|
104
|
+
return data.token
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// Refresh failed — fall through to auth error
|
|
108
|
+
}
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
66
112
|
/**
|
|
67
113
|
* @typedef {Object} RequestOptions
|
|
68
114
|
* @property {Record<string, string>} [headers] - Request headers
|
|
@@ -113,9 +159,8 @@ export async function request(path, options = {}) {
|
|
|
113
159
|
|
|
114
160
|
clearTimeout(timeoutId)
|
|
115
161
|
|
|
116
|
-
// Handle authentication errors
|
|
117
|
-
// Exception: 2FA OTP responses
|
|
118
|
-
// must be returned to the caller so the publish handler can prompt for OTP
|
|
162
|
+
// Handle authentication errors — try silent refresh before failing
|
|
163
|
+
// Exception: 2FA OTP responses must be returned to the caller
|
|
119
164
|
if (response.status === 401) {
|
|
120
165
|
const cloned = response.clone()
|
|
121
166
|
let data = {}
|
|
@@ -127,6 +172,16 @@ export async function request(path, options = {}) {
|
|
|
127
172
|
if (data.code === "OTP_REQUIRED" || data.code === "OTP_INVALID") {
|
|
128
173
|
return response
|
|
129
174
|
}
|
|
175
|
+
|
|
176
|
+
// Attempt silent refresh (Feature 44 Part B) — one retry on 401
|
|
177
|
+
if (attempt === 0) {
|
|
178
|
+
const refreshed = await tryRefresh()
|
|
179
|
+
if (refreshed) {
|
|
180
|
+
// Update the auth header and retry this request
|
|
181
|
+
headers.Authorization = `Bearer ${refreshed}`
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
}
|
|
130
185
|
throw new Error(ERROR_MESSAGES.notAuthenticated)
|
|
131
186
|
}
|
|
132
187
|
|
package/lib/commands/login.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
1
2
|
import http from "node:http"
|
|
3
|
+
import { hostname, userInfo } from "node:os"
|
|
2
4
|
import open from "open"
|
|
3
5
|
import { request } from "../api.js"
|
|
4
|
-
import { getRegistryUrl, setToken } from "../config.js"
|
|
6
|
+
import { getRegistryUrl, setRefreshToken, setToken } from "../config.js"
|
|
5
7
|
import { WARNING_MESSAGES } from "../constants.js"
|
|
6
8
|
import { createSpinner, log, printHeader } from "../ui.js"
|
|
7
9
|
|
|
@@ -188,11 +190,52 @@ export async function login() {
|
|
|
188
190
|
res.setHeader("Access-Control-Allow-Origin", "*")
|
|
189
191
|
|
|
190
192
|
if (url.pathname === "/callback") {
|
|
191
|
-
|
|
193
|
+
// Exchange code flow (Feature 42) — code is exchanged for token server-side
|
|
194
|
+
const exchangeCode = url.searchParams.get("code")
|
|
195
|
+
// Legacy fallback: direct token in URL (for older server versions)
|
|
196
|
+
const directToken = url.searchParams.get("token")
|
|
197
|
+
|
|
198
|
+
let token = directToken
|
|
199
|
+
let refreshToken = null
|
|
200
|
+
if (exchangeCode && !directToken) {
|
|
201
|
+
try {
|
|
202
|
+
const exchangeRes = await fetch(`${registryUrl}/api/cli/exchange`, {
|
|
203
|
+
method: "POST",
|
|
204
|
+
headers: { "Content-Type": "application/json" },
|
|
205
|
+
body: JSON.stringify({ code: exchangeCode }),
|
|
206
|
+
})
|
|
207
|
+
if (exchangeRes.ok) {
|
|
208
|
+
const data = await exchangeRes.json()
|
|
209
|
+
token = data.token
|
|
210
|
+
// Feature 44: store refresh token if server provides one
|
|
211
|
+
if (data.refreshToken) {
|
|
212
|
+
refreshToken = data.refreshToken
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
const err = await exchangeRes.json().catch(() => ({}))
|
|
216
|
+
throw new Error(err.error || "Exchange failed")
|
|
217
|
+
}
|
|
218
|
+
} catch (err) {
|
|
219
|
+
spinner.fail(`Token exchange failed: ${err.message}`)
|
|
220
|
+
res.writeHead(200, { "Content-Type": "text/html" })
|
|
221
|
+
res.end(
|
|
222
|
+
getErrorHtml(
|
|
223
|
+
"Exchange Failed",
|
|
224
|
+
"Failed to exchange the authorization code. Please try again.",
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
closeAndExit(res)
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
}
|
|
192
231
|
|
|
193
232
|
if (token) {
|
|
194
|
-
//
|
|
233
|
+
// Store access token
|
|
195
234
|
await setToken(token)
|
|
235
|
+
// Store refresh token for session auth (Feature 44 Part B)
|
|
236
|
+
if (refreshToken) {
|
|
237
|
+
await setRefreshToken(refreshToken)
|
|
238
|
+
}
|
|
196
239
|
|
|
197
240
|
try {
|
|
198
241
|
const response = await request("/-/whoami")
|
|
@@ -535,10 +578,15 @@ export async function login() {
|
|
|
535
578
|
}
|
|
536
579
|
})
|
|
537
580
|
|
|
581
|
+
// Compute device fingerprint for session auth (Feature 44 Part B)
|
|
582
|
+
const fp = createHash("sha256")
|
|
583
|
+
.update(`${hostname()}:${userInfo().username}:lpm-cli`)
|
|
584
|
+
.digest("hex")
|
|
585
|
+
const dn = encodeURIComponent(hostname())
|
|
586
|
+
|
|
538
587
|
server.listen(0, async () => {
|
|
539
588
|
const port = server.address().port
|
|
540
|
-
|
|
541
|
-
const loginUrl = `${registryUrl}/cli/login?port=${port}`
|
|
589
|
+
const loginUrl = `${registryUrl}/cli/login?port=${port}&fp=${fp}&dn=${dn}`
|
|
542
590
|
|
|
543
591
|
console.log(`Opening browser to: ${loginUrl}`)
|
|
544
592
|
await open(loginUrl)
|
package/lib/commands/npmrc.js
CHANGED
|
@@ -82,8 +82,9 @@ export async function npmrc(options) {
|
|
|
82
82
|
: `${registryUrl}/api/registry`
|
|
83
83
|
const registryHost = fullRegistryUrl.replace(/^https?:/, "")
|
|
84
84
|
|
|
85
|
-
//
|
|
86
|
-
|
|
85
|
+
// Default: scoped (only @lpm.dev packages through lpm.dev).
|
|
86
|
+
// --proxy: route all npm traffic through lpm.dev (Pro/Org feature).
|
|
87
|
+
let useScoped = !options?.proxy
|
|
87
88
|
|
|
88
89
|
// Read existing .npmrc and check for custom default registry
|
|
89
90
|
let npmrcContent = ""
|
package/lib/commands/setup.js
CHANGED
|
@@ -75,8 +75,9 @@ export async function setup(options) {
|
|
|
75
75
|
: `${registryUrl}/api/registry`
|
|
76
76
|
const registryHost = fullRegistryUrl.replace(/^https?:/, "")
|
|
77
77
|
|
|
78
|
-
//
|
|
79
|
-
|
|
78
|
+
// Default: scoped (only @lpm.dev packages through lpm.dev).
|
|
79
|
+
// --proxy: route all npm traffic through lpm.dev (Pro/Org feature).
|
|
80
|
+
let useScoped = !options?.proxy
|
|
80
81
|
|
|
81
82
|
// ── OIDC mode: exchange CI token for a real short-lived install token ──
|
|
82
83
|
if (options?.oidc) {
|
package/lib/config.js
CHANGED
|
@@ -118,6 +118,26 @@ export async function clearToken() {
|
|
|
118
118
|
config.delete("token")
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Store a refresh token in secure storage (Feature 44 Part B).
|
|
123
|
+
* Uses the same secure-store as auth tokens with a "refresh:" prefix.
|
|
124
|
+
* @param {string} refreshToken
|
|
125
|
+
* @returns {Promise<void>}
|
|
126
|
+
*/
|
|
127
|
+
export async function setRefreshToken(refreshToken) {
|
|
128
|
+
const registryUrl = getRegistryUrl()
|
|
129
|
+
await secureSetToken(refreshToken, `refresh:${registryUrl}`)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get the stored refresh token from secure storage.
|
|
134
|
+
* @returns {Promise<string|null>}
|
|
135
|
+
*/
|
|
136
|
+
export async function getRefreshToken() {
|
|
137
|
+
const registryUrl = getRegistryUrl()
|
|
138
|
+
return secureGetToken(`refresh:${registryUrl}`)
|
|
139
|
+
}
|
|
140
|
+
|
|
121
141
|
// ============================================================================
|
|
122
142
|
// Registry URL Configuration
|
|
123
143
|
// ============================================================================
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lpm-registry/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "CLI for Licensed Package Manager",
|
|
5
5
|
"author": "LPM <hello@lpm.dev> (https://lpm.dev)",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/lpm-dev/
|
|
8
|
+
"url": "https://github.com/lpm-dev/rust-client"
|
|
9
9
|
},
|
|
10
10
|
"homepage": "https://lpm.dev",
|
|
11
11
|
"license": "ISC",
|
|
@@ -61,11 +61,11 @@
|
|
|
61
61
|
"xcode": "^3.0.1"
|
|
62
62
|
},
|
|
63
63
|
"optionalDependencies": {
|
|
64
|
-
"@lpm-registry/cli-darwin-arm64": "0.
|
|
65
|
-
"@lpm-registry/cli-darwin-x64": "0.
|
|
66
|
-
"@lpm-registry/cli-linux-x64": "0.
|
|
67
|
-
"@lpm-registry/cli-linux-arm64": "0.
|
|
68
|
-
"@lpm-registry/cli-win32-x64": "0.
|
|
64
|
+
"@lpm-registry/cli-darwin-arm64": "0.9.2",
|
|
65
|
+
"@lpm-registry/cli-darwin-x64": "0.9.2",
|
|
66
|
+
"@lpm-registry/cli-linux-x64": "0.9.2",
|
|
67
|
+
"@lpm-registry/cli-linux-arm64": "0.9.2",
|
|
68
|
+
"@lpm-registry/cli-win32-x64": "0.9.2",
|
|
69
69
|
"keytar": "^7.9.0"
|
|
70
70
|
},
|
|
71
71
|
"type": "module",
|