@skillmarkdown/cli 0.1.5 → 0.2.0
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 +34 -2
- package/dist/cli.js +7 -3
- package/dist/commands/login.js +117 -0
- package/dist/commands/logout.js +19 -0
- package/dist/lib/auth-config.js +67 -0
- package/dist/lib/auth-defaults.js +8 -0
- package/dist/lib/auth-session.js +49 -0
- package/dist/lib/cli-text.js +4 -2
- package/dist/lib/firebase-auth.js +67 -0
- package/dist/lib/github-device-flow.js +86 -0
- package/dist/lib/http.js +44 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## Status
|
|
6
6
|
|
|
7
|
-
Early development.
|
|
7
|
+
Early development. Current command surface includes `skillmd init`, `skillmd validate`, `skillmd login`, and `skillmd logout`. Docs are intentionally lightweight and may evolve.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -64,9 +64,41 @@ Compare local validation with `skills-ref` (when installed):
|
|
|
64
64
|
skillmd validate --parity
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
### Login with GitHub (Device Flow)
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
skillmd login
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
By default, `login` uses the project’s built-in development config. You can override values with shell env vars or `~/.skillmd/.env`:
|
|
74
|
+
|
|
75
|
+
- `SKILLMD_GITHUB_CLIENT_ID`
|
|
76
|
+
- `SKILLMD_FIREBASE_API_KEY`
|
|
77
|
+
|
|
78
|
+
See `.env.example` for the expected keys.
|
|
79
|
+
Maintainers: built-in defaults are defined in `src/lib/auth-defaults.ts`.
|
|
80
|
+
|
|
81
|
+
Example override file:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
mkdir -p ~/.skillmd
|
|
85
|
+
cp .env.example ~/.skillmd/.env
|
|
86
|
+
# then edit values in ~/.skillmd/.env if needed
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Session helpers:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
skillmd login --status
|
|
93
|
+
skillmd login --reauth
|
|
94
|
+
skillmd logout
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
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.
|
|
98
|
+
|
|
67
99
|
## Development
|
|
68
100
|
|
|
69
|
-
- Local testing guide: `docs/testing.md`
|
|
101
|
+
- Local testing guide (includes manual `login` auth checks): `docs/testing.md`
|
|
70
102
|
- CI check script: `npm run ci:check`
|
|
71
103
|
- Packed tarball smoke test: `npm run smoke:pack`
|
|
72
104
|
- Optional npm link smoke test: `npm run smoke:link`
|
package/dist/cli.js
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const init_1 = require("./commands/init");
|
|
5
|
+
const login_1 = require("./commands/login");
|
|
6
|
+
const logout_1 = require("./commands/logout");
|
|
5
7
|
const validate_1 = require("./commands/validate");
|
|
6
8
|
const cli_text_1 = require("./lib/cli-text");
|
|
7
9
|
const COMMAND_HANDLERS = {
|
|
8
10
|
init: init_1.runInitCommand,
|
|
9
11
|
validate: validate_1.runValidateCommand,
|
|
12
|
+
login: login_1.runLoginCommand,
|
|
13
|
+
logout: logout_1.runLogoutCommand,
|
|
10
14
|
};
|
|
11
|
-
function main() {
|
|
15
|
+
async function main() {
|
|
12
16
|
const args = process.argv.slice(2);
|
|
13
17
|
const command = args[0];
|
|
14
18
|
if (args.length === 0) {
|
|
@@ -19,11 +23,11 @@ function main() {
|
|
|
19
23
|
}
|
|
20
24
|
const handler = COMMAND_HANDLERS[command];
|
|
21
25
|
if (handler) {
|
|
22
|
-
process.exitCode = handler(args.slice(1));
|
|
26
|
+
process.exitCode = await handler(args.slice(1));
|
|
23
27
|
return;
|
|
24
28
|
}
|
|
25
29
|
console.error(`skillmd: unknown command '${command}'`);
|
|
26
30
|
console.error(cli_text_1.ROOT_USAGE);
|
|
27
31
|
process.exitCode = 1;
|
|
28
32
|
}
|
|
29
|
-
main();
|
|
33
|
+
void main();
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runLoginCommand = runLoginCommand;
|
|
4
|
+
const cli_text_1 = require("../lib/cli-text");
|
|
5
|
+
const command_output_1 = require("../lib/command-output");
|
|
6
|
+
const auth_config_1 = require("../lib/auth-config");
|
|
7
|
+
const auth_session_1 = require("../lib/auth-session");
|
|
8
|
+
const github_device_flow_1 = require("../lib/github-device-flow");
|
|
9
|
+
const firebase_auth_1 = require("../lib/firebase-auth");
|
|
10
|
+
function parseFlags(args) {
|
|
11
|
+
let status = false;
|
|
12
|
+
let reauth = false;
|
|
13
|
+
for (const arg of args) {
|
|
14
|
+
if (arg === "--status") {
|
|
15
|
+
status = true;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (arg === "--reauth") {
|
|
19
|
+
reauth = true;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
return { status: false, reauth: false, valid: false };
|
|
23
|
+
}
|
|
24
|
+
if (status && reauth) {
|
|
25
|
+
return { status: false, reauth: false, valid: false };
|
|
26
|
+
}
|
|
27
|
+
return { status, reauth, valid: true };
|
|
28
|
+
}
|
|
29
|
+
function printSessionStatus(session) {
|
|
30
|
+
if (!session) {
|
|
31
|
+
console.log("Not logged in.");
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
if (session.email) {
|
|
35
|
+
console.log(`Logged in with GitHub as ${session.email}.`);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
console.log(`Logged in with GitHub (uid: ${session.uid}).`);
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
function requireConfig(env) {
|
|
42
|
+
try {
|
|
43
|
+
return (0, auth_config_1.getLoginEnvConfig)(env);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const message = error instanceof Error ? error.message : "invalid login configuration";
|
|
47
|
+
const wrapped = new Error(`${message}.`);
|
|
48
|
+
wrapped.cause = error;
|
|
49
|
+
throw wrapped;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function runLoginCommand(args, options = {}) {
|
|
53
|
+
const { status, reauth, valid } = parseFlags(args);
|
|
54
|
+
if (!valid) {
|
|
55
|
+
return (0, command_output_1.failWithUsage)("skillmd login: unsupported argument(s)", cli_text_1.LOGIN_USAGE);
|
|
56
|
+
}
|
|
57
|
+
const readSessionFn = options.readSession ?? auth_session_1.readAuthSession;
|
|
58
|
+
const writeSessionFn = options.writeSession ?? auth_session_1.writeAuthSession;
|
|
59
|
+
const clearSessionFn = options.clearSession ?? auth_session_1.clearAuthSession;
|
|
60
|
+
if (status) {
|
|
61
|
+
return printSessionStatus(readSessionFn());
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const config = requireConfig(options.env ?? process.env);
|
|
65
|
+
const existingSession = readSessionFn();
|
|
66
|
+
if (existingSession && !reauth) {
|
|
67
|
+
const verifyRefreshTokenFn = options.verifyRefreshToken ?? firebase_auth_1.verifyFirebaseRefreshToken;
|
|
68
|
+
try {
|
|
69
|
+
const validation = await verifyRefreshTokenFn(config.firebaseApiKey, existingSession.refreshToken);
|
|
70
|
+
if (validation.valid) {
|
|
71
|
+
if (existingSession.email) {
|
|
72
|
+
console.log(`Already logged in as ${existingSession.email}. Run 'skillmd logout' first.`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
console.log("Already logged in. Run 'skillmd logout' first.");
|
|
76
|
+
}
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
clearSessionFn();
|
|
80
|
+
console.log("Existing session is no longer valid. Starting re-authentication.");
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
84
|
+
console.error(`skillmd login: unable to verify existing session (${message}). ` +
|
|
85
|
+
"Keeping current session. Run 'skillmd login --reauth' to force reauthentication.");
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const requestDeviceCodeFn = options.requestDeviceCode ?? github_device_flow_1.requestDeviceCode;
|
|
90
|
+
const pollForAccessTokenFn = options.pollForAccessToken ?? github_device_flow_1.pollForAccessToken;
|
|
91
|
+
const signInFn = options.signInWithGitHubAccessToken ?? firebase_auth_1.signInWithGitHubAccessToken;
|
|
92
|
+
const deviceCode = await requestDeviceCodeFn(config.githubClientId);
|
|
93
|
+
console.log("Open this URL in your browser to authorize skillmd:");
|
|
94
|
+
console.log(deviceCode.verificationUriComplete ?? deviceCode.verificationUri);
|
|
95
|
+
console.log(`Then enter code: ${deviceCode.userCode}`);
|
|
96
|
+
const token = await pollForAccessTokenFn(config.githubClientId, deviceCode.deviceCode, deviceCode.interval, deviceCode.expiresIn);
|
|
97
|
+
const firebaseSession = await signInFn(config.firebaseApiKey, token.accessToken);
|
|
98
|
+
writeSessionFn({
|
|
99
|
+
provider: "github",
|
|
100
|
+
uid: firebaseSession.localId,
|
|
101
|
+
email: firebaseSession.email,
|
|
102
|
+
refreshToken: firebaseSession.refreshToken,
|
|
103
|
+
});
|
|
104
|
+
if (firebaseSession.email) {
|
|
105
|
+
console.log(`Login successful. Signed in as ${firebaseSession.email}.`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.log("Login successful.");
|
|
109
|
+
}
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
114
|
+
console.error(`skillmd login: ${message}`);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runLogoutCommand = runLogoutCommand;
|
|
4
|
+
const cli_text_1 = require("../lib/cli-text");
|
|
5
|
+
const command_output_1 = require("../lib/command-output");
|
|
6
|
+
const auth_session_1 = require("../lib/auth-session");
|
|
7
|
+
function runLogoutCommand(args, options = {}) {
|
|
8
|
+
if (args.length > 0) {
|
|
9
|
+
return (0, command_output_1.failWithUsage)("skillmd logout: unsupported argument(s)", cli_text_1.LOGOUT_USAGE);
|
|
10
|
+
}
|
|
11
|
+
const clearSessionFn = options.clearSession ?? auth_session_1.clearAuthSession;
|
|
12
|
+
const removed = clearSessionFn();
|
|
13
|
+
if (removed) {
|
|
14
|
+
console.log("Logged out.");
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
console.log("No active session to log out.");
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getLoginEnvConfig = getLoginEnvConfig;
|
|
4
|
+
exports.getDefaultUserEnvPath = getDefaultUserEnvPath;
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_os_1 = require("node:os");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const auth_defaults_1 = require("./auth-defaults");
|
|
9
|
+
const USER_ENV_RELATIVE_PATH = ".skillmd/.env";
|
|
10
|
+
function parseDotEnv(content) {
|
|
11
|
+
const values = {};
|
|
12
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
13
|
+
const line = rawLine.trim();
|
|
14
|
+
if (!line || line.startsWith("#")) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const equalIndex = line.indexOf("=");
|
|
18
|
+
if (equalIndex <= 0) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const key = line.slice(0, equalIndex).trim();
|
|
22
|
+
if (!key) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
let value = line.slice(equalIndex + 1).trim();
|
|
26
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
27
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
28
|
+
value = value.slice(1, -1);
|
|
29
|
+
}
|
|
30
|
+
values[key] = value;
|
|
31
|
+
}
|
|
32
|
+
return values;
|
|
33
|
+
}
|
|
34
|
+
function loadDotEnv(dotEnvPath) {
|
|
35
|
+
if (!(0, node_fs_1.existsSync)(dotEnvPath)) {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return parseDotEnv((0, node_fs_1.readFileSync)(dotEnvPath, "utf8"));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function pickValue(...candidates) {
|
|
46
|
+
for (const candidate of candidates) {
|
|
47
|
+
if (candidate && candidate.trim()) {
|
|
48
|
+
return candidate.trim();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
function getLoginEnvConfig(env = process.env, options = {}) {
|
|
54
|
+
const dotEnv = loadDotEnv(getDefaultUserEnvPath(options));
|
|
55
|
+
const githubClientId = pickValue(env.SKILLMD_GITHUB_CLIENT_ID, dotEnv.SKILLMD_GITHUB_CLIENT_ID, auth_defaults_1.DEFAULT_LOGIN_AUTH_CONFIG.githubClientId);
|
|
56
|
+
const firebaseApiKey = pickValue(env.SKILLMD_FIREBASE_API_KEY, dotEnv.SKILLMD_FIREBASE_API_KEY, auth_defaults_1.DEFAULT_LOGIN_AUTH_CONFIG.firebaseApiKey);
|
|
57
|
+
if (!githubClientId || !firebaseApiKey) {
|
|
58
|
+
throw new Error("missing login configuration");
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
githubClientId,
|
|
62
|
+
firebaseApiKey,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function getDefaultUserEnvPath(options = {}) {
|
|
66
|
+
return (0, node_path_1.join)(options.homeDir ?? (0, node_os_1.homedir)(), USER_ENV_RELATIVE_PATH);
|
|
67
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_LOGIN_AUTH_CONFIG = void 0;
|
|
4
|
+
// Single source of truth for built-in auth defaults.
|
|
5
|
+
exports.DEFAULT_LOGIN_AUTH_CONFIG = Object.freeze({
|
|
6
|
+
githubClientId: "Ov23lixkdtyLp35IFaBG",
|
|
7
|
+
firebaseApiKey: "AIzaSyAkaZRmpCvZasFjeRAfW_b0V0nUcGOTjok",
|
|
8
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getDefaultSessionPath = getDefaultSessionPath;
|
|
4
|
+
exports.readAuthSession = readAuthSession;
|
|
5
|
+
exports.writeAuthSession = writeAuthSession;
|
|
6
|
+
exports.clearAuthSession = clearAuthSession;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_os_1 = require("node:os");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const SESSION_PATH = (0, node_path_1.join)((0, node_os_1.homedir)(), ".skillmd", "auth.json");
|
|
11
|
+
function getDefaultSessionPath() {
|
|
12
|
+
return SESSION_PATH;
|
|
13
|
+
}
|
|
14
|
+
function readAuthSession(sessionPath = SESSION_PATH) {
|
|
15
|
+
if (!(0, node_fs_1.existsSync)(sessionPath)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(sessionPath, "utf8"));
|
|
20
|
+
if (!parsed ||
|
|
21
|
+
parsed.provider !== "github" ||
|
|
22
|
+
typeof parsed.uid !== "string" ||
|
|
23
|
+
parsed.uid.length === 0 ||
|
|
24
|
+
typeof parsed.refreshToken !== "string" ||
|
|
25
|
+
parsed.refreshToken.length === 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (parsed.email !== undefined && typeof parsed.email !== "string") {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function writeAuthSession(session, sessionPath = SESSION_PATH) {
|
|
38
|
+
const parentDir = (0, node_path_1.dirname)(sessionPath);
|
|
39
|
+
(0, node_fs_1.mkdirSync)(parentDir, { recursive: true });
|
|
40
|
+
(0, node_fs_1.writeFileSync)(sessionPath, JSON.stringify(session, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
41
|
+
(0, node_fs_1.chmodSync)(sessionPath, 0o600);
|
|
42
|
+
}
|
|
43
|
+
function clearAuthSession(sessionPath = SESSION_PATH) {
|
|
44
|
+
if (!(0, node_fs_1.existsSync)(sessionPath)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
(0, node_fs_1.rmSync)(sessionPath, { force: true });
|
|
48
|
+
return true;
|
|
49
|
+
}
|
package/dist/lib/cli-text.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.VALIDATE_USAGE = exports.INIT_USAGE = exports.ROOT_USAGE = void 0;
|
|
4
|
-
exports.ROOT_USAGE = "Usage: skillmd <init|validate>";
|
|
3
|
+
exports.LOGOUT_USAGE = exports.LOGIN_USAGE = exports.VALIDATE_USAGE = exports.INIT_USAGE = exports.ROOT_USAGE = void 0;
|
|
4
|
+
exports.ROOT_USAGE = "Usage: skillmd <init|validate|login|logout>";
|
|
5
5
|
exports.INIT_USAGE = "Usage: skillmd init [--no-validate]";
|
|
6
6
|
exports.VALIDATE_USAGE = "Usage: skillmd validate [path] [--strict] [--parity]";
|
|
7
|
+
exports.LOGIN_USAGE = "Usage: skillmd login [--status|--reauth]";
|
|
8
|
+
exports.LOGOUT_USAGE = "Usage: skillmd logout";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.signInWithGitHubAccessToken = signInWithGitHubAccessToken;
|
|
4
|
+
exports.verifyFirebaseRefreshToken = verifyFirebaseRefreshToken;
|
|
5
|
+
const http_1 = require("./http");
|
|
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
|
+
}
|
|
16
|
+
async function signInWithGitHubAccessToken(apiKey, githubAccessToken) {
|
|
17
|
+
const response = await (0, http_1.fetchWithTimeout)(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp?key=${encodeURIComponent(apiKey)}`, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
},
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
requestUri: "http://localhost",
|
|
24
|
+
returnSecureToken: true,
|
|
25
|
+
returnIdpCredential: true,
|
|
26
|
+
postBody: `access_token=${encodeURIComponent(githubAccessToken)}&providerId=github.com`,
|
|
27
|
+
}),
|
|
28
|
+
}, { timeoutMs: FIREBASE_HTTP_TIMEOUT_MS });
|
|
29
|
+
const payload = await parseJsonApiResponse(response, "Firebase auth API");
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const message = payload.error?.message || "Firebase signInWithIdp request failed";
|
|
32
|
+
throw new Error(`Firebase auth error: ${message}`);
|
|
33
|
+
}
|
|
34
|
+
if (!payload.localId || !payload.refreshToken) {
|
|
35
|
+
throw new Error("Firebase auth response was missing required fields");
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
localId: payload.localId,
|
|
39
|
+
email: payload.email,
|
|
40
|
+
refreshToken: payload.refreshToken,
|
|
41
|
+
};
|
|
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" || errorMessage === "TOKEN_EXPIRED")) {
|
|
64
|
+
return { valid: false };
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`Firebase token verification failed (${response.status}): ${errorMessage || "unknown error"}`);
|
|
67
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requestDeviceCode = requestDeviceCode;
|
|
4
|
+
exports.pollForAccessToken = pollForAccessToken;
|
|
5
|
+
const http_1 = require("./http");
|
|
6
|
+
const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
|
|
7
|
+
const GITHUB_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
8
|
+
const GITHUB_HTTP_TIMEOUT_MS = 10000;
|
|
9
|
+
async function postGitHubForm(url, form) {
|
|
10
|
+
const response = await (0, http_1.fetchWithTimeout)(url, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: {
|
|
13
|
+
Accept: "application/json",
|
|
14
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
15
|
+
},
|
|
16
|
+
body: form,
|
|
17
|
+
}, { timeoutMs: GITHUB_HTTP_TIMEOUT_MS });
|
|
18
|
+
const text = await response.text();
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(text);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error(`GitHub API returned non-JSON response (${response.status})`);
|
|
25
|
+
}
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`GitHub API request failed (${response.status})`);
|
|
28
|
+
}
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
async function requestDeviceCode(clientId, scope = "read:user user:email") {
|
|
32
|
+
const payload = await postGitHubForm(GITHUB_DEVICE_CODE_URL, new URLSearchParams({
|
|
33
|
+
client_id: clientId,
|
|
34
|
+
scope,
|
|
35
|
+
}));
|
|
36
|
+
if (!payload.device_code ||
|
|
37
|
+
!payload.user_code ||
|
|
38
|
+
!payload.verification_uri ||
|
|
39
|
+
!payload.expires_in) {
|
|
40
|
+
throw new Error("GitHub device code response was missing required fields");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
deviceCode: payload.device_code,
|
|
44
|
+
userCode: payload.user_code,
|
|
45
|
+
verificationUri: payload.verification_uri,
|
|
46
|
+
verificationUriComplete: payload.verification_uri_complete,
|
|
47
|
+
expiresIn: payload.expires_in,
|
|
48
|
+
interval: payload.interval ?? 5,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function sleep(ms) {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
setTimeout(resolve, ms);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async function pollForAccessToken(clientId, deviceCode, intervalSeconds, expiresInSeconds, options = {}) {
|
|
57
|
+
const startedAt = Date.now();
|
|
58
|
+
let pollInterval = Math.max(1, intervalSeconds);
|
|
59
|
+
const sleepFn = options.sleep ?? sleep;
|
|
60
|
+
while (Date.now() - startedAt < expiresInSeconds * 1000) {
|
|
61
|
+
await sleepFn(pollInterval * 1000);
|
|
62
|
+
const payload = await postGitHubForm(GITHUB_ACCESS_TOKEN_URL, new URLSearchParams({
|
|
63
|
+
client_id: clientId,
|
|
64
|
+
device_code: deviceCode,
|
|
65
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
66
|
+
}));
|
|
67
|
+
if (payload.access_token) {
|
|
68
|
+
return { accessToken: payload.access_token };
|
|
69
|
+
}
|
|
70
|
+
if (!payload.error || payload.error === "authorization_pending") {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (payload.error === "slow_down") {
|
|
74
|
+
pollInterval += 5;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (payload.error === "expired_token") {
|
|
78
|
+
throw new Error("GitHub device code expired before authorization completed");
|
|
79
|
+
}
|
|
80
|
+
if (payload.error === "access_denied") {
|
|
81
|
+
throw new Error("GitHub authorization was denied by the user");
|
|
82
|
+
}
|
|
83
|
+
throw new Error(payload.error_description || `GitHub OAuth error: ${payload.error}`);
|
|
84
|
+
}
|
|
85
|
+
throw new Error("GitHub device login timed out");
|
|
86
|
+
}
|
package/dist/lib/http.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchWithTimeout = fetchWithTimeout;
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
5
|
+
async function fetchWithTimeout(input, init = {}, options = {}) {
|
|
6
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
7
|
+
const controller = new AbortController();
|
|
8
|
+
const userSignal = init.signal;
|
|
9
|
+
let abortListener;
|
|
10
|
+
if (userSignal) {
|
|
11
|
+
if (userSignal.aborted) {
|
|
12
|
+
controller.abort(userSignal.reason);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
abortListener = () => {
|
|
16
|
+
controller.abort(userSignal.reason);
|
|
17
|
+
};
|
|
18
|
+
userSignal.addEventListener("abort", abortListener, { once: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const timeout = setTimeout(() => {
|
|
22
|
+
controller.abort(new Error(`request timed out after ${timeoutMs}ms`));
|
|
23
|
+
}, timeoutMs);
|
|
24
|
+
try {
|
|
25
|
+
return await fetch(input, {
|
|
26
|
+
...init,
|
|
27
|
+
signal: controller.signal,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof Error && error.name === "AbortError" && !userSignal?.aborted) {
|
|
32
|
+
const timeoutError = new Error(`request timed out after ${timeoutMs}ms`);
|
|
33
|
+
timeoutError.cause = error;
|
|
34
|
+
throw timeoutError;
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
clearTimeout(timeout);
|
|
40
|
+
if (userSignal && abortListener) {
|
|
41
|
+
userSignal.removeEventListener("abort", abortListener);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|