@skillmarkdown/cli 0.1.6 → 0.2.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/README.md
CHANGED
|
@@ -74,6 +74,7 @@ By default, `login` uses the project’s built-in development config. You can ov
|
|
|
74
74
|
|
|
75
75
|
- `SKILLMD_GITHUB_CLIENT_ID`
|
|
76
76
|
- `SKILLMD_FIREBASE_API_KEY`
|
|
77
|
+
- `SKILLMD_FIREBASE_PROJECT_ID`
|
|
77
78
|
|
|
78
79
|
See `.env.example` for the expected keys.
|
|
79
80
|
Maintainers: built-in defaults are defined in `src/lib/auth-defaults.ts`.
|
|
@@ -94,6 +95,10 @@ skillmd login --reauth
|
|
|
94
95
|
skillmd logout
|
|
95
96
|
```
|
|
96
97
|
|
|
98
|
+
`skillmd login --status` includes the authenticated Firebase project so you can confirm whether the active session is for `skillmarkdown` or `skillmarkdown-development`.
|
|
99
|
+
|
|
100
|
+
When a saved session exists, `skillmd login` verifies the stored refresh token. If it is invalid/expired, the CLI automatically starts a new login flow. If verification is inconclusive (for example network timeout), the command exits non-zero and keeps the current session.
|
|
101
|
+
|
|
97
102
|
## Development
|
|
98
103
|
|
|
99
104
|
- Local testing guide (includes manual `login` auth checks): `docs/testing.md`
|
package/dist/commands/login.js
CHANGED
|
@@ -26,16 +26,34 @@ function parseFlags(args) {
|
|
|
26
26
|
}
|
|
27
27
|
return { status, reauth, valid: true };
|
|
28
28
|
}
|
|
29
|
-
function
|
|
29
|
+
function formatSessionProject(session, currentConfigProjectId) {
|
|
30
|
+
if (!session.projectId) {
|
|
31
|
+
if (currentConfigProjectId) {
|
|
32
|
+
return { label: `unknown (current config: ${currentConfigProjectId})`, mismatch: false };
|
|
33
|
+
}
|
|
34
|
+
return { label: "unknown", mismatch: false };
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
label: session.projectId,
|
|
38
|
+
mismatch: Boolean(currentConfigProjectId && session.projectId !== currentConfigProjectId),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function printSessionStatus(session, currentConfigProjectId) {
|
|
30
42
|
if (!session) {
|
|
31
43
|
console.log("Not logged in.");
|
|
32
44
|
return 1;
|
|
33
45
|
}
|
|
46
|
+
const project = formatSessionProject(session, currentConfigProjectId);
|
|
34
47
|
if (session.email) {
|
|
35
|
-
console.log(`Logged in with GitHub as ${session.email}.`);
|
|
36
|
-
|
|
48
|
+
console.log(`Logged in with GitHub as ${session.email} (project: ${project.label}).`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.log(`Logged in with GitHub (uid: ${session.uid}, project: ${project.label}).`);
|
|
52
|
+
}
|
|
53
|
+
if (project.mismatch && currentConfigProjectId) {
|
|
54
|
+
console.log(`Current CLI config targets project '${currentConfigProjectId}'. ` +
|
|
55
|
+
"Run 'skillmd login --reauth' to switch projects.");
|
|
37
56
|
}
|
|
38
|
-
console.log(`Logged in with GitHub (uid: ${session.uid}).`);
|
|
39
57
|
return 0;
|
|
40
58
|
}
|
|
41
59
|
function requireConfig(env) {
|
|
@@ -56,21 +74,43 @@ async function runLoginCommand(args, options = {}) {
|
|
|
56
74
|
}
|
|
57
75
|
const readSessionFn = options.readSession ?? auth_session_1.readAuthSession;
|
|
58
76
|
const writeSessionFn = options.writeSession ?? auth_session_1.writeAuthSession;
|
|
59
|
-
|
|
60
|
-
return printSessionStatus(readSessionFn());
|
|
61
|
-
}
|
|
62
|
-
const existingSession = readSessionFn();
|
|
63
|
-
if (existingSession && !reauth) {
|
|
64
|
-
if (existingSession.email) {
|
|
65
|
-
console.log(`Already logged in as ${existingSession.email}. Run 'skillmd logout' first.`);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
console.log("Already logged in. Run 'skillmd logout' first.");
|
|
69
|
-
}
|
|
70
|
-
return 0;
|
|
71
|
-
}
|
|
77
|
+
const clearSessionFn = options.clearSession ?? auth_session_1.clearAuthSession;
|
|
72
78
|
try {
|
|
73
79
|
const config = requireConfig(options.env ?? process.env);
|
|
80
|
+
if (status) {
|
|
81
|
+
return printSessionStatus(readSessionFn(), config.firebaseProjectId);
|
|
82
|
+
}
|
|
83
|
+
const existingSession = readSessionFn();
|
|
84
|
+
if (existingSession && !reauth) {
|
|
85
|
+
const verifyRefreshTokenFn = options.verifyRefreshToken ?? firebase_auth_1.verifyFirebaseRefreshToken;
|
|
86
|
+
try {
|
|
87
|
+
const validation = await verifyRefreshTokenFn(config.firebaseApiKey, existingSession.refreshToken);
|
|
88
|
+
if (validation.valid) {
|
|
89
|
+
const project = formatSessionProject(existingSession, config.firebaseProjectId);
|
|
90
|
+
if (existingSession.email) {
|
|
91
|
+
console.log(`Already logged in as ${existingSession.email} (project: ${project.label}). ` +
|
|
92
|
+
"Run 'skillmd logout' first.");
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.log(`Already logged in (uid: ${existingSession.uid}, project: ${project.label}). ` +
|
|
96
|
+
"Run 'skillmd logout' first.");
|
|
97
|
+
}
|
|
98
|
+
if (project.mismatch) {
|
|
99
|
+
console.log(`Current CLI config targets project '${config.firebaseProjectId}'. ` +
|
|
100
|
+
"Run 'skillmd login --reauth' to switch projects.");
|
|
101
|
+
}
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
clearSessionFn();
|
|
105
|
+
console.log("Existing session is no longer valid. Starting re-authentication.");
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
109
|
+
console.error(`skillmd login: unable to verify existing session (${message}). ` +
|
|
110
|
+
"Keeping current session. Run 'skillmd login --reauth' to force reauthentication.");
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
74
114
|
const requestDeviceCodeFn = options.requestDeviceCode ?? github_device_flow_1.requestDeviceCode;
|
|
75
115
|
const pollForAccessTokenFn = options.pollForAccessToken ?? github_device_flow_1.pollForAccessToken;
|
|
76
116
|
const signInFn = options.signInWithGitHubAccessToken ?? firebase_auth_1.signInWithGitHubAccessToken;
|
|
@@ -85,12 +125,13 @@ async function runLoginCommand(args, options = {}) {
|
|
|
85
125
|
uid: firebaseSession.localId,
|
|
86
126
|
email: firebaseSession.email,
|
|
87
127
|
refreshToken: firebaseSession.refreshToken,
|
|
128
|
+
projectId: config.firebaseProjectId,
|
|
88
129
|
});
|
|
89
130
|
if (firebaseSession.email) {
|
|
90
|
-
console.log(`Login successful. Signed in as ${firebaseSession.email}.`);
|
|
131
|
+
console.log(`Login successful. Signed in as ${firebaseSession.email} (project: ${config.firebaseProjectId}).`);
|
|
91
132
|
}
|
|
92
133
|
else {
|
|
93
|
-
console.log(
|
|
134
|
+
console.log(`Login successful (project: ${config.firebaseProjectId}).`);
|
|
94
135
|
}
|
|
95
136
|
return 0;
|
|
96
137
|
}
|
package/dist/lib/auth-config.js
CHANGED
|
@@ -54,12 +54,14 @@ function getLoginEnvConfig(env = process.env, options = {}) {
|
|
|
54
54
|
const dotEnv = loadDotEnv(getDefaultUserEnvPath(options));
|
|
55
55
|
const githubClientId = pickValue(env.SKILLMD_GITHUB_CLIENT_ID, dotEnv.SKILLMD_GITHUB_CLIENT_ID, auth_defaults_1.DEFAULT_LOGIN_AUTH_CONFIG.githubClientId);
|
|
56
56
|
const firebaseApiKey = pickValue(env.SKILLMD_FIREBASE_API_KEY, dotEnv.SKILLMD_FIREBASE_API_KEY, auth_defaults_1.DEFAULT_LOGIN_AUTH_CONFIG.firebaseApiKey);
|
|
57
|
-
|
|
57
|
+
const firebaseProjectId = pickValue(env.SKILLMD_FIREBASE_PROJECT_ID, dotEnv.SKILLMD_FIREBASE_PROJECT_ID, auth_defaults_1.DEFAULT_LOGIN_AUTH_CONFIG.firebaseProjectId);
|
|
58
|
+
if (!githubClientId || !firebaseApiKey || !firebaseProjectId) {
|
|
58
59
|
throw new Error("missing login configuration");
|
|
59
60
|
}
|
|
60
61
|
return {
|
|
61
62
|
githubClientId,
|
|
62
63
|
firebaseApiKey,
|
|
64
|
+
firebaseProjectId,
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
function getDefaultUserEnvPath(options = {}) {
|
package/dist/lib/auth-session.js
CHANGED
|
@@ -28,6 +28,12 @@ function readAuthSession(sessionPath = SESSION_PATH) {
|
|
|
28
28
|
if (parsed.email !== undefined && typeof parsed.email !== "string") {
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
|
+
if (parsed.projectId !== undefined && typeof parsed.projectId !== "string") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (typeof parsed.projectId === "string" && parsed.projectId.length === 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
31
37
|
return parsed;
|
|
32
38
|
}
|
|
33
39
|
catch {
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.signInWithGitHubAccessToken = signInWithGitHubAccessToken;
|
|
4
|
+
exports.verifyFirebaseRefreshToken = verifyFirebaseRefreshToken;
|
|
4
5
|
const http_1 = require("./http");
|
|
5
6
|
const FIREBASE_HTTP_TIMEOUT_MS = 10000;
|
|
7
|
+
async function parseJsonApiResponse(response, apiLabel) {
|
|
8
|
+
const text = await response.text();
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(text);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
throw new Error(`${apiLabel} returned non-JSON response (${response.status})`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
6
16
|
async function signInWithGitHubAccessToken(apiKey, githubAccessToken) {
|
|
7
17
|
const response = await (0, http_1.fetchWithTimeout)(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp?key=${encodeURIComponent(apiKey)}`, {
|
|
8
18
|
method: "POST",
|
|
@@ -16,14 +26,7 @@ async function signInWithGitHubAccessToken(apiKey, githubAccessToken) {
|
|
|
16
26
|
postBody: `access_token=${encodeURIComponent(githubAccessToken)}&providerId=github.com`,
|
|
17
27
|
}),
|
|
18
28
|
}, { timeoutMs: FIREBASE_HTTP_TIMEOUT_MS });
|
|
19
|
-
const
|
|
20
|
-
let payload;
|
|
21
|
-
try {
|
|
22
|
-
payload = JSON.parse(text);
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
throw new Error(`Firebase auth API returned non-JSON response (${response.status})`);
|
|
26
|
-
}
|
|
29
|
+
const payload = await parseJsonApiResponse(response, "Firebase auth API");
|
|
27
30
|
if (!response.ok) {
|
|
28
31
|
const message = payload.error?.message || "Firebase signInWithIdp request failed";
|
|
29
32
|
throw new Error(`Firebase auth error: ${message}`);
|
|
@@ -37,3 +40,30 @@ async function signInWithGitHubAccessToken(apiKey, githubAccessToken) {
|
|
|
37
40
|
refreshToken: payload.refreshToken,
|
|
38
41
|
};
|
|
39
42
|
}
|
|
43
|
+
async function verifyFirebaseRefreshToken(apiKey, refreshToken) {
|
|
44
|
+
const response = await (0, http_1.fetchWithTimeout)(`https://securetoken.googleapis.com/v1/token?key=${encodeURIComponent(apiKey)}`, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: {
|
|
47
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
48
|
+
},
|
|
49
|
+
body: new URLSearchParams({
|
|
50
|
+
grant_type: "refresh_token",
|
|
51
|
+
refresh_token: refreshToken,
|
|
52
|
+
}),
|
|
53
|
+
}, { timeoutMs: FIREBASE_HTTP_TIMEOUT_MS });
|
|
54
|
+
const payload = await parseJsonApiResponse(response, "Firebase token API");
|
|
55
|
+
if (response.ok) {
|
|
56
|
+
if (!payload.refresh_token || !payload.access_token) {
|
|
57
|
+
throw new Error("Firebase token API response was missing required fields");
|
|
58
|
+
}
|
|
59
|
+
return { valid: true };
|
|
60
|
+
}
|
|
61
|
+
const errorMessage = payload.error?.message ?? "";
|
|
62
|
+
if (response.status === 400 &&
|
|
63
|
+
(errorMessage === "INVALID_REFRESH_TOKEN" ||
|
|
64
|
+
errorMessage === "TOKEN_EXPIRED" ||
|
|
65
|
+
errorMessage === "PROJECT_NUMBER_MISMATCH")) {
|
|
66
|
+
return { valid: false };
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`Firebase token verification failed (${response.status}): ${errorMessage || "unknown error"}`);
|
|
69
|
+
}
|
|
@@ -53,11 +53,12 @@ function sleep(ms) {
|
|
|
53
53
|
setTimeout(resolve, ms);
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
|
-
async function pollForAccessToken(clientId, deviceCode, intervalSeconds, expiresInSeconds) {
|
|
56
|
+
async function pollForAccessToken(clientId, deviceCode, intervalSeconds, expiresInSeconds, options = {}) {
|
|
57
57
|
const startedAt = Date.now();
|
|
58
58
|
let pollInterval = Math.max(1, intervalSeconds);
|
|
59
|
+
const sleepFn = options.sleep ?? sleep;
|
|
59
60
|
while (Date.now() - startedAt < expiresInSeconds * 1000) {
|
|
60
|
-
await
|
|
61
|
+
await sleepFn(pollInterval * 1000);
|
|
61
62
|
const payload = await postGitHubForm(GITHUB_ACCESS_TOKEN_URL, new URLSearchParams({
|
|
62
63
|
client_id: clientId,
|
|
63
64
|
device_code: deviceCode,
|