@elixpo/lixblogs-cli 1.3.1 → 1.3.3
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/README.md +6 -3
- package/bin/lixblogs.mjs +18 -5
- package/package.json +1 -1
- package/src/cli/ui.js +3 -1
- package/src/commands/auth/login.js +16 -2
- package/src/commands/auth/profileAlias.js +29 -0
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@ node bin/lixblogs.mjs --help
|
|
|
29
29
|
```bash
|
|
30
30
|
# Log in via device authorization
|
|
31
31
|
node bin/lixblogs.mjs login
|
|
32
|
+
# Credentials are saved under the authenticated username and made active.
|
|
32
33
|
|
|
33
34
|
# Save credentials under an explicit local profile name
|
|
34
35
|
node bin/lixblogs.mjs login --profile personal
|
|
@@ -51,8 +52,10 @@ Interactive login shows one compact device card and stays quiet while Accounts
|
|
|
51
52
|
approval is pending. Press Enter to open the verification URL locally, use
|
|
52
53
|
`--open` to open it immediately, or copy the URL to any browser when connected
|
|
53
54
|
to a VPS. Device authorization does not require a localhost callback or an
|
|
54
|
-
exposed port.
|
|
55
|
-
|
|
55
|
+
exposed port. The authenticated username becomes the local profile alias unless
|
|
56
|
+
`--profile` explicitly overrides it. Run `lixblogs login` again to add another
|
|
57
|
+
account, `lixblogs profiles` to list saved accounts, and
|
|
58
|
+
`lixblogs use <username>` to switch the active one.
|
|
56
59
|
|
|
57
60
|
### Blog lifecycle
|
|
58
61
|
|
|
@@ -148,7 +151,7 @@ command exits with code 3 and retains both versions under
|
|
|
148
151
|
`.lixblogs-conflicts/`; it never overwrites the newer server revision.
|
|
149
152
|
|
|
150
153
|
Global flags:
|
|
151
|
-
- `--profile <name>` —
|
|
154
|
+
- `--profile <name>` — override the username-based local account alias
|
|
152
155
|
- `--env <environment>` — override environment (`development` | `staging` | `production`)
|
|
153
156
|
- `--scope <scope>` — request an additional/alternate OAuth scope; repeatable
|
|
154
157
|
- `--open` — open the verification URL with the device code pre-filled
|
package/bin/lixblogs.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import { authStatus } from "../src/commands/auth/status.js";
|
|
|
31
31
|
import { authLogout } from "../src/commands/auth/logout.js";
|
|
32
32
|
import { authRevoke } from "../src/commands/auth/revoke.js";
|
|
33
33
|
import { authProfiles, authUse } from "../src/commands/auth/profiles.js";
|
|
34
|
+
import { profileAliasFromIdentity } from "../src/commands/auth/profileAlias.js";
|
|
34
35
|
import { ProfileRegistry, validateProfileId } from "../src/config/ProfileRegistry.js";
|
|
35
36
|
import { AuthenticatedClient } from "../src/auth/AuthenticatedClient.js";
|
|
36
37
|
import { BlogClient, BlogApiError } from "../src/api/BlogClient.js";
|
|
@@ -164,7 +165,7 @@ Usage:
|
|
|
164
165
|
lixblogs skill install <name> [--target <directory>] [--dry-run] --yes
|
|
165
166
|
|
|
166
167
|
Global flags:
|
|
167
|
-
--profile <name>
|
|
168
|
+
--profile <name> local account alias (defaults to the signed-in username)
|
|
168
169
|
--env <environment> override environment (development|staging|production)
|
|
169
170
|
--auth-provider <provider> elixpo, or mock in development/test only
|
|
170
171
|
--accounts-url <url> override the Accounts discovery origin
|
|
@@ -269,7 +270,11 @@ async function getCredentialStoreOrFail(opts, profileRegistry) {
|
|
|
269
270
|
async function runLogin(opts) {
|
|
270
271
|
const config = resolveConfig({ flags: configFlags(opts) });
|
|
271
272
|
const profileRegistry = new ProfileRegistry();
|
|
272
|
-
const
|
|
273
|
+
const requestedProfileId = validateProfileId(config.profile);
|
|
274
|
+
const scopes = opts.scope?.length ? [...opts.scope] : [...DEFAULT_SCOPES];
|
|
275
|
+
if (!config.profileExplicit && !scopes.includes("lixblogs:profile:read")) {
|
|
276
|
+
scopes.push("lixblogs:profile:read");
|
|
277
|
+
}
|
|
273
278
|
|
|
274
279
|
let provider;
|
|
275
280
|
try {
|
|
@@ -287,9 +292,15 @@ async function runLogin(opts) {
|
|
|
287
292
|
result = await authLogin({
|
|
288
293
|
provider,
|
|
289
294
|
credentialStore,
|
|
290
|
-
profileId,
|
|
291
|
-
scopes
|
|
295
|
+
profileId: requestedProfileId,
|
|
296
|
+
scopes,
|
|
292
297
|
openBrowser: opts.open ? openBrowser : undefined,
|
|
298
|
+
resolveProfileId: config.profileExplicit
|
|
299
|
+
? undefined
|
|
300
|
+
: ({ accessToken }) => profileAliasFromIdentity({
|
|
301
|
+
accessToken,
|
|
302
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
303
|
+
}),
|
|
293
304
|
onStatus: (status) => {
|
|
294
305
|
if (opts.json) {
|
|
295
306
|
if (status.type !== "pending") output(opts, { event: status.type, ...status });
|
|
@@ -303,7 +314,7 @@ async function runLogin(opts) {
|
|
|
303
314
|
url,
|
|
304
315
|
code: status.userCode,
|
|
305
316
|
expiresInSeconds: status.expiresInSeconds,
|
|
306
|
-
profile:
|
|
317
|
+
profile: config.profileExplicit ? requestedProfileId : null,
|
|
307
318
|
interactive,
|
|
308
319
|
color: colorEnabled(),
|
|
309
320
|
}));
|
|
@@ -331,6 +342,8 @@ async function runLogin(opts) {
|
|
|
331
342
|
output(opts, { ok: true, profile: result.profileId });
|
|
332
343
|
if (!opts.json && !opts.quiet) {
|
|
333
344
|
console.log(` Credentials saved to local profile "${result.profileId}".`);
|
|
345
|
+
console.log(" Tip: add another account with `lixblogs login`, list accounts with `lixblogs profiles`,");
|
|
346
|
+
console.log(" and switch with `lixblogs use <username>`.");
|
|
334
347
|
}
|
|
335
348
|
}
|
|
336
349
|
|
package/package.json
CHANGED
package/src/cli/ui.js
CHANGED
|
@@ -27,7 +27,9 @@ export function loginChallenge({ url, code, expiresInSeconds, profile, interacti
|
|
|
27
27
|
` URL ${url}`,
|
|
28
28
|
` Code ${paint(code, ANSI.bold, color)}`,
|
|
29
29
|
` Expires ${Math.ceil(expiresInSeconds / 60)} min`,
|
|
30
|
-
|
|
30
|
+
profile
|
|
31
|
+
? ` Profile ${profile} ${paint("(local credential slot)", ANSI.dim, color)}`
|
|
32
|
+
: ` Profile ${paint("your Accounts username after approval", ANSI.dim, color)}`,
|
|
31
33
|
"",
|
|
32
34
|
` ${instruction}`,
|
|
33
35
|
" No localhost callback or exposed port is required.",
|
|
@@ -23,6 +23,7 @@ import { redactErrorMessage } from "../../config/redact.js";
|
|
|
23
23
|
* @param {string} params.profileId - which named profile this login is for
|
|
24
24
|
* @param {string[]} params.scopes - scopes being requested
|
|
25
25
|
* @param {(url: string) => Promise<void>} [params.openBrowser] - optional browser opener
|
|
26
|
+
* @param {(params: { accessToken: string, requestedProfileId: string }) => Promise<string>} [params.resolveProfileId]
|
|
26
27
|
* @param {(ms: number) => Promise<void>} [params.sleep] - injectable for tests
|
|
27
28
|
* @param {(...args: any[]) => void} [params.onStatus] - callback for UI updates (verification URL, polling status, etc.)
|
|
28
29
|
* @returns {Promise<{ ok: true, profileId: string } | { ok: false, reason: string }>}
|
|
@@ -33,6 +34,7 @@ export async function authLogin({
|
|
|
33
34
|
profileId,
|
|
34
35
|
scopes,
|
|
35
36
|
openBrowser,
|
|
37
|
+
resolveProfileId,
|
|
36
38
|
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
37
39
|
onStatus = () => {},
|
|
38
40
|
}) {
|
|
@@ -69,14 +71,26 @@ export async function authLogin({
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
if (result.status === "approved") {
|
|
72
|
-
|
|
74
|
+
let resolvedProfileId = profileId;
|
|
75
|
+
if (resolveProfileId) {
|
|
76
|
+
try {
|
|
77
|
+
resolvedProfileId = await resolveProfileId({
|
|
78
|
+
accessToken: result.token.accessToken,
|
|
79
|
+
requestedProfileId: profileId,
|
|
80
|
+
});
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return { ok: false, reason: redactErrorMessage(err.message) };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await credentialStore.set(resolvedProfileId, {
|
|
73
87
|
accessToken: result.token.accessToken,
|
|
74
88
|
refreshToken: result.token.refreshToken,
|
|
75
89
|
expiresAt: Date.now() + result.token.expiresInSeconds * 1000,
|
|
76
90
|
scopes: result.token.scopes,
|
|
77
91
|
});
|
|
78
92
|
onStatus({ type: "approved" });
|
|
79
|
-
return { ok: true, profileId };
|
|
93
|
+
return { ok: true, profileId: resolvedProfileId };
|
|
80
94
|
}
|
|
81
95
|
|
|
82
96
|
if (result.status === "denied") {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { validateProfileId } from "../../config/ProfileRegistry.js";
|
|
2
|
+
|
|
3
|
+
/** Resolve the authenticated Accounts username without persisting a temporary profile. */
|
|
4
|
+
export async function profileAliasFromIdentity({
|
|
5
|
+
accessToken,
|
|
6
|
+
apiBaseUrl,
|
|
7
|
+
fetchImpl = globalThis.fetch,
|
|
8
|
+
}) {
|
|
9
|
+
const endpoint = new URL("/api/v1/me", apiBaseUrl);
|
|
10
|
+
const response = await fetchImpl(endpoint, {
|
|
11
|
+
headers: {
|
|
12
|
+
accept: "application/json",
|
|
13
|
+
authorization: `Bearer ${accessToken}`,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
let payload;
|
|
18
|
+
try {
|
|
19
|
+
payload = await response.json();
|
|
20
|
+
} catch {
|
|
21
|
+
throw new Error("LixBlogs could not resolve the signed-in username.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!response.ok || typeof payload?.data?.username !== "string") {
|
|
25
|
+
throw new Error(payload?.error?.message || "LixBlogs could not resolve the signed-in username.");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return validateProfileId(payload.data.username);
|
|
29
|
+
}
|