signet-auth 1.0.0-beta.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.
Potentially problematic release.
This version of signet-auth might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +393 -0
- package/bin/sig.js +65 -0
- package/dist/auth-manager.d.ts +90 -0
- package/dist/auth-manager.js +262 -0
- package/dist/browser/adapters/playwright.adapter.d.ts +14 -0
- package/dist/browser/adapters/playwright.adapter.js +188 -0
- package/dist/browser/flows/form-login.flow.d.ts +6 -0
- package/dist/browser/flows/form-login.flow.js +35 -0
- package/dist/browser/flows/header-capture.d.ts +23 -0
- package/dist/browser/flows/header-capture.js +104 -0
- package/dist/browser/flows/hybrid-flow.d.ts +37 -0
- package/dist/browser/flows/hybrid-flow.js +104 -0
- package/dist/browser/flows/oauth-consent.flow.d.ts +20 -0
- package/dist/browser/flows/oauth-consent.flow.js +170 -0
- package/dist/cli/commands/doctor.d.ts +6 -0
- package/dist/cli/commands/doctor.js +263 -0
- package/dist/cli/commands/get.d.ts +2 -0
- package/dist/cli/commands/get.js +83 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.js +244 -0
- package/dist/cli/commands/login.d.ts +2 -0
- package/dist/cli/commands/login.js +77 -0
- package/dist/cli/commands/logout.d.ts +2 -0
- package/dist/cli/commands/logout.js +11 -0
- package/dist/cli/commands/providers.d.ts +2 -0
- package/dist/cli/commands/providers.js +30 -0
- package/dist/cli/commands/remote.d.ts +1 -0
- package/dist/cli/commands/remote.js +67 -0
- package/dist/cli/commands/request.d.ts +2 -0
- package/dist/cli/commands/request.js +82 -0
- package/dist/cli/commands/status.d.ts +2 -0
- package/dist/cli/commands/status.js +41 -0
- package/dist/cli/commands/sync.d.ts +2 -0
- package/dist/cli/commands/sync.js +62 -0
- package/dist/cli/formatters.d.ts +3 -0
- package/dist/cli/formatters.js +25 -0
- package/dist/cli/main.d.ts +8 -0
- package/dist/cli/main.js +125 -0
- package/dist/config/generator.d.ts +24 -0
- package/dist/config/generator.js +97 -0
- package/dist/config/loader.d.ts +21 -0
- package/dist/config/loader.js +54 -0
- package/dist/config/schema.d.ts +44 -0
- package/dist/config/schema.js +8 -0
- package/dist/config/validator.d.ts +15 -0
- package/dist/config/validator.js +228 -0
- package/dist/core/errors.d.ts +57 -0
- package/dist/core/errors.js +107 -0
- package/dist/core/interfaces/auth-strategy.d.ts +48 -0
- package/dist/core/interfaces/auth-strategy.js +1 -0
- package/dist/core/interfaces/browser-adapter.d.ts +73 -0
- package/dist/core/interfaces/browser-adapter.js +1 -0
- package/dist/core/interfaces/provider.d.ts +15 -0
- package/dist/core/interfaces/provider.js +1 -0
- package/dist/core/interfaces/storage.d.ts +21 -0
- package/dist/core/interfaces/storage.js +1 -0
- package/dist/core/result.d.ts +21 -0
- package/dist/core/result.js +16 -0
- package/dist/core/types.d.ts +128 -0
- package/dist/core/types.js +6 -0
- package/dist/deps.d.ts +20 -0
- package/dist/deps.js +54 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +37 -0
- package/dist/providers/auto-provision.d.ts +9 -0
- package/dist/providers/auto-provision.js +27 -0
- package/dist/providers/config-loader.d.ts +7 -0
- package/dist/providers/config-loader.js +7 -0
- package/dist/providers/provider-registry.d.ts +19 -0
- package/dist/providers/provider-registry.js +68 -0
- package/dist/storage/cached-storage.d.ts +24 -0
- package/dist/storage/cached-storage.js +57 -0
- package/dist/storage/directory-storage.d.ts +25 -0
- package/dist/storage/directory-storage.js +184 -0
- package/dist/storage/memory-storage.d.ts +14 -0
- package/dist/storage/memory-storage.js +27 -0
- package/dist/strategies/api-token.strategy.d.ts +6 -0
- package/dist/strategies/api-token.strategy.js +63 -0
- package/dist/strategies/basic-auth.strategy.d.ts +6 -0
- package/dist/strategies/basic-auth.strategy.js +41 -0
- package/dist/strategies/cookie.strategy.d.ts +6 -0
- package/dist/strategies/cookie.strategy.js +118 -0
- package/dist/strategies/oauth2.strategy.d.ts +6 -0
- package/dist/strategies/oauth2.strategy.js +134 -0
- package/dist/strategies/registry.d.ts +13 -0
- package/dist/strategies/registry.js +25 -0
- package/dist/sync/remote-config.d.ts +8 -0
- package/dist/sync/remote-config.js +49 -0
- package/dist/sync/sync-engine.d.ts +10 -0
- package/dist/sync/sync-engine.js +96 -0
- package/dist/sync/transports/ssh.d.ts +18 -0
- package/dist/sync/transports/ssh.js +115 -0
- package/dist/sync/types.d.ts +17 -0
- package/dist/sync/types.js +1 -0
- package/dist/utils/duration.d.ts +9 -0
- package/dist/utils/duration.js +34 -0
- package/dist/utils/http.d.ts +4 -0
- package/dist/utils/http.js +10 -0
- package/dist/utils/jwt.d.ts +15 -0
- package/dist/utils/jwt.js +30 -0
- package/package.json +56 -0
- package/src/auth-manager.ts +331 -0
- package/src/browser/adapters/playwright.adapter.ts +247 -0
- package/src/browser/flows/form-login.flow.ts +35 -0
- package/src/browser/flows/header-capture.ts +128 -0
- package/src/browser/flows/hybrid-flow.ts +165 -0
- package/src/browser/flows/oauth-consent.flow.ts +200 -0
- package/src/cli/commands/doctor.ts +301 -0
- package/src/cli/commands/get.ts +96 -0
- package/src/cli/commands/init.ts +289 -0
- package/src/cli/commands/login.ts +94 -0
- package/src/cli/commands/logout.ts +17 -0
- package/src/cli/commands/providers.ts +39 -0
- package/src/cli/commands/remote.ts +71 -0
- package/src/cli/commands/request.ts +97 -0
- package/src/cli/commands/status.ts +48 -0
- package/src/cli/commands/sync.ts +71 -0
- package/src/cli/formatters.ts +31 -0
- package/src/cli/main.ts +144 -0
- package/src/config/generator.ts +122 -0
- package/src/config/loader.ts +70 -0
- package/src/config/schema.ts +75 -0
- package/src/config/validator.ts +281 -0
- package/src/core/errors.ts +182 -0
- package/src/core/interfaces/auth-strategy.ts +65 -0
- package/src/core/interfaces/browser-adapter.ts +81 -0
- package/src/core/interfaces/provider.ts +19 -0
- package/src/core/interfaces/storage.ts +26 -0
- package/src/core/result.ts +24 -0
- package/src/core/types.ts +194 -0
- package/src/deps.ts +80 -0
- package/src/index.ts +109 -0
- package/src/providers/auto-provision.ts +30 -0
- package/src/providers/config-loader.ts +8 -0
- package/src/providers/provider-registry.ts +79 -0
- package/src/storage/cached-storage.ts +72 -0
- package/src/storage/directory-storage.ts +204 -0
- package/src/storage/memory-storage.ts +35 -0
- package/src/strategies/api-token.strategy.ts +87 -0
- package/src/strategies/basic-auth.strategy.ts +64 -0
- package/src/strategies/cookie.strategy.ts +153 -0
- package/src/strategies/oauth2.strategy.ts +178 -0
- package/src/strategies/registry.ts +34 -0
- package/src/sync/remote-config.ts +60 -0
- package/src/sync/sync-engine.ts +113 -0
- package/src/sync/transports/ssh.ts +130 -0
- package/src/sync/types.ts +15 -0
- package/src/utils/duration.ts +34 -0
- package/src/utils/http.ts +11 -0
- package/src/utils/jwt.ts +39 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { err } from '../../core/result.js';
|
|
2
|
+
import { BrowserError, BrowserTimeoutError } from '../../core/errors.js';
|
|
3
|
+
import { startHeaderCapture } from './header-capture.js';
|
|
4
|
+
/**
|
|
5
|
+
* Hybrid browser flow: headless → visible fallback.
|
|
6
|
+
*
|
|
7
|
+
* 1. Tries headless browser first (fast, no user interaction)
|
|
8
|
+
* 2. If headless fails (timeout, CAPTCHA, cert dialog), switches to visible
|
|
9
|
+
* 3. In visible mode, waits for user to complete login manually
|
|
10
|
+
* 4. Extracts credentials once authenticated
|
|
11
|
+
*
|
|
12
|
+
* This flow is adapter-agnostic — works with any IBrowserAdapter.
|
|
13
|
+
*/
|
|
14
|
+
export async function runHybridFlow(adapter, options) {
|
|
15
|
+
const { headlessTimeout, visibleTimeout } = options.browserConfig;
|
|
16
|
+
// Phase 1: Try headless (unless forceVisible)
|
|
17
|
+
if (!options.forceVisible) {
|
|
18
|
+
const headlessResult = await attemptAuth(adapter, {
|
|
19
|
+
...options,
|
|
20
|
+
headless: true,
|
|
21
|
+
timeout: headlessTimeout,
|
|
22
|
+
});
|
|
23
|
+
if (headlessResult.ok)
|
|
24
|
+
return headlessResult;
|
|
25
|
+
console.error(`[signet] Headless auth failed: ${headlessResult.error.message}. Switching to visible mode...`);
|
|
26
|
+
}
|
|
27
|
+
// Phase 2: Visible mode (user-assisted)
|
|
28
|
+
console.error('[signet] Please complete login in the browser window...');
|
|
29
|
+
return await attemptAuth(adapter, {
|
|
30
|
+
...options,
|
|
31
|
+
headless: false,
|
|
32
|
+
timeout: visibleTimeout,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async function attemptAuth(adapter, options) {
|
|
36
|
+
let session;
|
|
37
|
+
let headerCleanup;
|
|
38
|
+
try {
|
|
39
|
+
const launchOptions = {
|
|
40
|
+
headless: options.headless,
|
|
41
|
+
timeout: options.timeout,
|
|
42
|
+
args: options.browserArgs,
|
|
43
|
+
};
|
|
44
|
+
session = await adapter.launch(launchOptions);
|
|
45
|
+
const page = await session.newPage();
|
|
46
|
+
// Set up x-header capture before navigation (so we capture all traffic)
|
|
47
|
+
let xHeaders;
|
|
48
|
+
if (options.xHeaders && options.xHeaders.length > 0) {
|
|
49
|
+
const capture = startHeaderCapture(page, options.xHeaders, options.providerDomains ?? []);
|
|
50
|
+
xHeaders = capture.xHeaders;
|
|
51
|
+
headerCleanup = capture.cleanup;
|
|
52
|
+
}
|
|
53
|
+
// Navigate to entry URL
|
|
54
|
+
// Strategy can override global waitUntil (e.g. cookie forces 'networkidle')
|
|
55
|
+
const waitUntil = options.waitUntil ?? options.browserConfig.waitUntil;
|
|
56
|
+
await page.goto(options.entryUrl, {
|
|
57
|
+
waitUntil,
|
|
58
|
+
timeout: options.timeout,
|
|
59
|
+
});
|
|
60
|
+
// Brief pause to let any client-side redirects start
|
|
61
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
62
|
+
// Check if already authenticated (cached session/cookies)
|
|
63
|
+
if (await options.isAuthenticated(page)) {
|
|
64
|
+
const result = await options.extractCredentials(page, xHeaders, { immediateAuth: true });
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
// Wait for authentication to complete (polling)
|
|
68
|
+
const authenticated = await pollForAuth(page, options.isAuthenticated, options.timeout);
|
|
69
|
+
if (!authenticated) {
|
|
70
|
+
return err(new BrowserTimeoutError('waiting for authentication', options.timeout));
|
|
71
|
+
}
|
|
72
|
+
const result = await options.extractCredentials(page, xHeaders, { immediateAuth: false });
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
if (e instanceof BrowserTimeoutError) {
|
|
77
|
+
return err(e);
|
|
78
|
+
}
|
|
79
|
+
return err(new BrowserError(e.message));
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
if (headerCleanup) {
|
|
83
|
+
headerCleanup();
|
|
84
|
+
}
|
|
85
|
+
if (session) {
|
|
86
|
+
await session.close().catch(() => { });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function pollForAuth(page, isAuthenticated, timeoutMs) {
|
|
91
|
+
const pollInterval = 2_000;
|
|
92
|
+
const deadline = Date.now() + timeoutMs;
|
|
93
|
+
while (Date.now() < deadline) {
|
|
94
|
+
try {
|
|
95
|
+
if (await isAuthenticated(page))
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Page might be navigating — ignore and retry
|
|
100
|
+
}
|
|
101
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IBrowserPage } from '../../core/interfaces/browser-adapter.js';
|
|
2
|
+
import type { BearerCredential } from '../../core/types.js';
|
|
3
|
+
import type { Result } from '../../core/result.js';
|
|
4
|
+
import { type AuthError } from '../../core/errors.js';
|
|
5
|
+
/**
|
|
6
|
+
* Extract OAuth tokens from browser localStorage.
|
|
7
|
+
* Searches for JWT-like strings and validates them against expected audiences.
|
|
8
|
+
*/
|
|
9
|
+
export declare function extractOAuthTokens(page: IBrowserPage, options?: {
|
|
10
|
+
audiences?: string[];
|
|
11
|
+
extractRefreshToken?: boolean;
|
|
12
|
+
/** Max retries to wait for tokens to appear (default: 5, 2s apart) */
|
|
13
|
+
maxRetries?: number;
|
|
14
|
+
}): Promise<Result<BearerCredential, AuthError>>;
|
|
15
|
+
/**
|
|
16
|
+
* Check if OAuth tokens (JWTs) exist in browser storage.
|
|
17
|
+
* Used as a guard to ensure MSAL has finished writing tokens before
|
|
18
|
+
* the browser is closed.
|
|
19
|
+
*/
|
|
20
|
+
export declare function hasOAuthTokens(page: IBrowserPage, audiences?: string[]): Promise<boolean>;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { ok, err } from '../../core/result.js';
|
|
2
|
+
import { BrowserError } from '../../core/errors.js';
|
|
3
|
+
import { decodeJwt } from '../../utils/jwt.js';
|
|
4
|
+
const EXPIRY_BUFFER_MS = 60_000; // 1 minute buffer
|
|
5
|
+
/** Returns true if the JWT payload has a valid (non-expired) exp claim. */
|
|
6
|
+
function isTokenValid(payload) {
|
|
7
|
+
if (!payload?.exp)
|
|
8
|
+
return false;
|
|
9
|
+
return payload.exp * 1000 > Date.now() + EXPIRY_BUFFER_MS;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Extract OAuth tokens from browser localStorage.
|
|
13
|
+
* Searches for JWT-like strings and validates them against expected audiences.
|
|
14
|
+
*/
|
|
15
|
+
export async function extractOAuthTokens(page, options) {
|
|
16
|
+
const maxRetries = options?.maxRetries ?? 5;
|
|
17
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
18
|
+
const result = await tryExtractTokens(page, options);
|
|
19
|
+
if (result.ok)
|
|
20
|
+
return result;
|
|
21
|
+
// If this isn't the last attempt, wait and retry (MSAL may still be writing tokens)
|
|
22
|
+
if (attempt < maxRetries) {
|
|
23
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
return result; // Return the last error
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return err(new BrowserError('Token extraction exhausted all retries.'));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Check if OAuth tokens (JWTs) exist in browser storage.
|
|
33
|
+
* Used as a guard to ensure MSAL has finished writing tokens before
|
|
34
|
+
* the browser is closed.
|
|
35
|
+
*/
|
|
36
|
+
export async function hasOAuthTokens(page, audiences) {
|
|
37
|
+
try {
|
|
38
|
+
const storage = await page.evaluate(() => {
|
|
39
|
+
const entries = {};
|
|
40
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
41
|
+
const key = localStorage.key(i);
|
|
42
|
+
if (key)
|
|
43
|
+
entries['local:' + key] = localStorage.getItem(key) ?? '';
|
|
44
|
+
}
|
|
45
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
46
|
+
const key = sessionStorage.key(i);
|
|
47
|
+
if (key)
|
|
48
|
+
entries['session:' + key] = sessionStorage.getItem(key) ?? '';
|
|
49
|
+
}
|
|
50
|
+
return entries;
|
|
51
|
+
});
|
|
52
|
+
const jwtRegex = /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
|
|
53
|
+
const tokens = [];
|
|
54
|
+
for (const value of Object.values(storage)) {
|
|
55
|
+
const matches = value.match(jwtRegex);
|
|
56
|
+
if (matches)
|
|
57
|
+
tokens.push(...matches);
|
|
58
|
+
}
|
|
59
|
+
if (tokens.length === 0)
|
|
60
|
+
return false;
|
|
61
|
+
for (const token of tokens) {
|
|
62
|
+
const payload = decodeJwt(token);
|
|
63
|
+
if (!payload || !isTokenValid(payload))
|
|
64
|
+
continue;
|
|
65
|
+
if (audiences && audiences.length > 0) {
|
|
66
|
+
const aud = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
|
|
67
|
+
if (audiences.some(a => aud.includes(a)))
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function tryExtractTokens(page, options) {
|
|
81
|
+
try {
|
|
82
|
+
// Extract all entries from both localStorage and sessionStorage
|
|
83
|
+
const storage = await page.evaluate(() => {
|
|
84
|
+
const entries = {};
|
|
85
|
+
// Check localStorage
|
|
86
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
87
|
+
const key = localStorage.key(i);
|
|
88
|
+
if (key)
|
|
89
|
+
entries['local:' + key] = localStorage.getItem(key) ?? '';
|
|
90
|
+
}
|
|
91
|
+
// Check sessionStorage
|
|
92
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
93
|
+
const key = sessionStorage.key(i);
|
|
94
|
+
if (key)
|
|
95
|
+
entries['session:' + key] = sessionStorage.getItem(key) ?? '';
|
|
96
|
+
}
|
|
97
|
+
return entries;
|
|
98
|
+
});
|
|
99
|
+
// Find JWTs in localStorage values
|
|
100
|
+
const jwtRegex = /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
|
|
101
|
+
const tokens = [];
|
|
102
|
+
for (const value of Object.values(storage)) {
|
|
103
|
+
const matches = value.match(jwtRegex);
|
|
104
|
+
if (matches)
|
|
105
|
+
tokens.push(...matches);
|
|
106
|
+
}
|
|
107
|
+
if (tokens.length === 0) {
|
|
108
|
+
return err(new BrowserError('No OAuth tokens found in browser localStorage.'));
|
|
109
|
+
}
|
|
110
|
+
// Find the best matching token: must be non-expired, match audience if specified,
|
|
111
|
+
// and prefer the one with the latest expiry.
|
|
112
|
+
let bestToken;
|
|
113
|
+
let bestPayload;
|
|
114
|
+
for (const token of tokens) {
|
|
115
|
+
const payload = decodeJwt(token);
|
|
116
|
+
if (!payload || !isTokenValid(payload))
|
|
117
|
+
continue;
|
|
118
|
+
if (options?.audiences && options.audiences.length > 0) {
|
|
119
|
+
const aud = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
|
|
120
|
+
if (!options.audiences.some(a => aud.includes(a)))
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// Prefer the token with the latest expiry
|
|
124
|
+
if (!bestPayload || (payload.exp ?? 0) > (bestPayload.exp ?? 0)) {
|
|
125
|
+
bestToken = token;
|
|
126
|
+
bestPayload = payload;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!bestToken) {
|
|
130
|
+
return err(new BrowserError(`No valid (non-expired) token matching audiences [${options?.audiences?.join(', ')}] found. ` +
|
|
131
|
+
`Found ${tokens.length} token(s) in storage.`));
|
|
132
|
+
}
|
|
133
|
+
// Extract refresh token (MSAL format: keys containing "refreshtoken")
|
|
134
|
+
let refreshToken;
|
|
135
|
+
if (options?.extractRefreshToken) {
|
|
136
|
+
for (const [rawKey, value] of Object.entries(storage)) {
|
|
137
|
+
// Strip the 'local:' or 'session:' prefix for key matching
|
|
138
|
+
const key = rawKey.replace(/^(local|session):/, '');
|
|
139
|
+
if (key.toLowerCase().includes('refreshtoken')) {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(value);
|
|
142
|
+
if (parsed.secret) {
|
|
143
|
+
refreshToken = parsed.secret;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Not JSON — skip
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const expiresAt = bestPayload?.exp
|
|
154
|
+
? new Date(bestPayload.exp * 1000).toISOString()
|
|
155
|
+
: undefined;
|
|
156
|
+
const credential = {
|
|
157
|
+
type: 'bearer',
|
|
158
|
+
accessToken: bestToken,
|
|
159
|
+
refreshToken,
|
|
160
|
+
expiresAt,
|
|
161
|
+
scopes: bestPayload?.scp
|
|
162
|
+
? String(bestPayload.scp).split(' ')
|
|
163
|
+
: undefined,
|
|
164
|
+
};
|
|
165
|
+
return ok(credential);
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
return err(new BrowserError(`Token extraction failed: ${e.message}`));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sig doctor — Environment diagnostics.
|
|
3
|
+
* Runs a series of checks and reports pass/fail for each.
|
|
4
|
+
* Does NOT take deps (can run before config is fully wired).
|
|
5
|
+
*/
|
|
6
|
+
export declare function runDoctor(positionals: string[], flags: Record<string, string | boolean>): Promise<void>;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sig doctor — Environment diagnostics.
|
|
3
|
+
* Runs a series of checks and reports pass/fail for each.
|
|
4
|
+
* Does NOT take deps (can run before config is fully wired).
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import fsp from 'node:fs/promises';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
11
|
+
import { getConfigPath, loadConfig } from '../../config/loader.js';
|
|
12
|
+
import { isOk } from '../../core/result.js';
|
|
13
|
+
const PASS = '\u2713'; // ✓
|
|
14
|
+
const FAIL = '\u2717'; // ✗
|
|
15
|
+
function printResults(results) {
|
|
16
|
+
let failures = 0;
|
|
17
|
+
for (const r of results) {
|
|
18
|
+
if (r.ok) {
|
|
19
|
+
const detail = r.detail ? ` (${r.detail})` : '';
|
|
20
|
+
console.log(` ${PASS} ${r.label}${detail}`);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
failures++;
|
|
24
|
+
console.log(` ${FAIL} ${r.label}`);
|
|
25
|
+
if (r.hint) {
|
|
26
|
+
console.log(` \u2192 ${r.hint}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
console.log('');
|
|
31
|
+
if (failures === 0) {
|
|
32
|
+
console.log('All checks passed.');
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
console.log(`${failures} issue${failures > 1 ? 's' : ''} found.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Individual checks
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
function checkConfigExists() {
|
|
42
|
+
const configPath = getConfigPath();
|
|
43
|
+
const exists = fs.existsSync(configPath);
|
|
44
|
+
return {
|
|
45
|
+
label: 'Config file exists',
|
|
46
|
+
ok: exists,
|
|
47
|
+
detail: exists ? configPath.replace(os.homedir(), '~') : undefined,
|
|
48
|
+
hint: exists ? undefined : 'Run "sig init" to create ~/.signet/config.yaml',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function checkConfigValid() {
|
|
52
|
+
const configResult = await loadConfig();
|
|
53
|
+
if (!isOk(configResult)) {
|
|
54
|
+
return {
|
|
55
|
+
label: 'Config is valid',
|
|
56
|
+
ok: false,
|
|
57
|
+
hint: configResult.error.message,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
label: 'Config is valid',
|
|
62
|
+
ok: true,
|
|
63
|
+
config: configResult.value,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function checkCredentialsDir(config) {
|
|
67
|
+
if (!config) {
|
|
68
|
+
return {
|
|
69
|
+
label: 'Credentials directory exists',
|
|
70
|
+
ok: false,
|
|
71
|
+
hint: 'Fix config first',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const dir = config.storage.credentialsDir.replace(/^~/, os.homedir());
|
|
75
|
+
try {
|
|
76
|
+
await fsp.access(dir, fs.constants.R_OK | fs.constants.W_OK);
|
|
77
|
+
return {
|
|
78
|
+
label: 'Credentials directory exists',
|
|
79
|
+
ok: true,
|
|
80
|
+
detail: config.storage.credentialsDir,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return {
|
|
85
|
+
label: 'Credentials directory exists',
|
|
86
|
+
ok: false,
|
|
87
|
+
hint: `Directory not found or not writable: ${config.storage.credentialsDir}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function checkBrowserDataDir(config) {
|
|
92
|
+
if (!config) {
|
|
93
|
+
return {
|
|
94
|
+
label: 'Browser data directory exists',
|
|
95
|
+
ok: false,
|
|
96
|
+
hint: 'Fix config first',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const dir = config.browser.browserDataDir.replace(/^~/, os.homedir());
|
|
100
|
+
const exists = fs.existsSync(dir);
|
|
101
|
+
return {
|
|
102
|
+
label: 'Browser data directory exists',
|
|
103
|
+
ok: exists,
|
|
104
|
+
detail: exists ? config.browser.browserDataDir : undefined,
|
|
105
|
+
hint: exists ? undefined : `Directory not found: ${config.browser.browserDataDir}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function findChannelBrowser(channel) {
|
|
109
|
+
const platform = process.platform;
|
|
110
|
+
// Check common browser locations based on channel and platform
|
|
111
|
+
if (platform === 'darwin') {
|
|
112
|
+
const apps = {
|
|
113
|
+
chrome: '/Applications/Google Chrome.app',
|
|
114
|
+
msedge: '/Applications/Microsoft Edge.app',
|
|
115
|
+
chromium: '/Applications/Chromium.app',
|
|
116
|
+
};
|
|
117
|
+
if (apps[channel])
|
|
118
|
+
return fs.existsSync(apps[channel]);
|
|
119
|
+
}
|
|
120
|
+
if (platform === 'linux') {
|
|
121
|
+
const bins = {
|
|
122
|
+
chrome: 'google-chrome',
|
|
123
|
+
msedge: 'microsoft-edge',
|
|
124
|
+
chromium: 'chromium',
|
|
125
|
+
};
|
|
126
|
+
if (bins[channel]) {
|
|
127
|
+
try {
|
|
128
|
+
execSync(`which ${bins[channel]}`, { stdio: 'ignore' });
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
async function checkBrowserAvailable(config) {
|
|
139
|
+
if (!config) {
|
|
140
|
+
return {
|
|
141
|
+
label: 'Browser available',
|
|
142
|
+
ok: false,
|
|
143
|
+
hint: 'Fix config first',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const channel = config.browser.channel;
|
|
147
|
+
try {
|
|
148
|
+
// Verify playwright-core is importable
|
|
149
|
+
await import('playwright-core');
|
|
150
|
+
// Check if the channel browser is installed on the system
|
|
151
|
+
const found = findChannelBrowser(channel);
|
|
152
|
+
if (found) {
|
|
153
|
+
return {
|
|
154
|
+
label: 'Browser available',
|
|
155
|
+
ok: true,
|
|
156
|
+
detail: channel,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// Fallback: if we can't detect by channel but playwright-core is available, report cautiously
|
|
160
|
+
return {
|
|
161
|
+
label: 'Browser available',
|
|
162
|
+
ok: true,
|
|
163
|
+
detail: `${channel} (playwright-core loaded, browser not verified)`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return {
|
|
168
|
+
label: 'Browser available',
|
|
169
|
+
ok: false,
|
|
170
|
+
hint: 'playwright-core not installed. Run "npm install playwright-core".',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function checkNodeVersion() {
|
|
175
|
+
const version = process.version;
|
|
176
|
+
const match = version.match(/^v(\d+)/);
|
|
177
|
+
const major = match ? parseInt(match[1], 10) : 0;
|
|
178
|
+
return {
|
|
179
|
+
label: 'Node.js version',
|
|
180
|
+
ok: major >= 18,
|
|
181
|
+
detail: version,
|
|
182
|
+
hint: major < 18 ? `Node.js >= 18 is required. Current: ${version}` : undefined,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
async function checkStoredCredentials(config) {
|
|
186
|
+
if (!config) {
|
|
187
|
+
return {
|
|
188
|
+
label: 'Stored credentials',
|
|
189
|
+
ok: true,
|
|
190
|
+
detail: 'skipped (no config)',
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const dir = config.storage.credentialsDir.replace(/^~/, os.homedir());
|
|
194
|
+
try {
|
|
195
|
+
const files = await fsp.readdir(dir);
|
|
196
|
+
const jsonFiles = files.filter(f => f.endsWith('.json') && !f.endsWith('.lock'));
|
|
197
|
+
const total = jsonFiles.length;
|
|
198
|
+
// Check for expired credentials by reading each file
|
|
199
|
+
let expired = 0;
|
|
200
|
+
for (const file of jsonFiles) {
|
|
201
|
+
try {
|
|
202
|
+
const content = await fsp.readFile(path.join(dir, file), 'utf-8');
|
|
203
|
+
const data = JSON.parse(content);
|
|
204
|
+
const cred = data?.credential;
|
|
205
|
+
if (cred?.type === 'bearer' && cred?.token) {
|
|
206
|
+
try {
|
|
207
|
+
const { isJwtExpired } = await import('../../utils/jwt.js');
|
|
208
|
+
if (isJwtExpired(cred.token))
|
|
209
|
+
expired++;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// JWT decode failed, skip
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// Skip unreadable files
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const detail = expired > 0
|
|
221
|
+
? `${total} stored credential${total !== 1 ? 's' : ''} (${expired} expired)`
|
|
222
|
+
: `${total} stored credential${total !== 1 ? 's' : ''}`;
|
|
223
|
+
return {
|
|
224
|
+
label: 'Stored credentials',
|
|
225
|
+
ok: true,
|
|
226
|
+
detail,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return {
|
|
231
|
+
label: 'Stored credentials',
|
|
232
|
+
ok: true,
|
|
233
|
+
detail: '0 stored credentials',
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Main command
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
export async function runDoctor(positionals, flags) {
|
|
241
|
+
const results = [];
|
|
242
|
+
// a. Config file exists
|
|
243
|
+
results.push(checkConfigExists());
|
|
244
|
+
// b. Config is valid
|
|
245
|
+
const configCheck = await checkConfigValid();
|
|
246
|
+
results.push(configCheck);
|
|
247
|
+
const config = configCheck.config;
|
|
248
|
+
// c. Credentials directory
|
|
249
|
+
results.push(await checkCredentialsDir(config));
|
|
250
|
+
// d. Browser data directory
|
|
251
|
+
results.push(await checkBrowserDataDir(config));
|
|
252
|
+
// e. Browser available
|
|
253
|
+
results.push(await checkBrowserAvailable(config));
|
|
254
|
+
// f. Node.js version
|
|
255
|
+
results.push(checkNodeVersion());
|
|
256
|
+
// g. Stored credentials
|
|
257
|
+
results.push(await checkStoredCredentials(config));
|
|
258
|
+
printResults(results);
|
|
259
|
+
const hasFailures = results.some(r => !r.ok);
|
|
260
|
+
if (hasFailures) {
|
|
261
|
+
process.exitCode = 1;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { isOk } from '../../core/result.js';
|
|
2
|
+
import { formatJson, formatCredentialHeaders } from '../formatters.js';
|
|
3
|
+
const PRIMARY_HEADERS = ['cookie', 'authorization'];
|
|
4
|
+
export async function runGet(positionals, flags, deps) {
|
|
5
|
+
const target = positionals[0];
|
|
6
|
+
if (!target) {
|
|
7
|
+
process.stderr.write('Usage: sig get <provider|url>\n');
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const isUrl = target.startsWith('http://') || target.startsWith('https://');
|
|
12
|
+
let providerId;
|
|
13
|
+
let credential;
|
|
14
|
+
if (isUrl) {
|
|
15
|
+
const result = await deps.authManager.getCredentialsByUrl(target);
|
|
16
|
+
if (!isOk(result)) {
|
|
17
|
+
process.stderr.write(`Error: ${result.error.message}\n`);
|
|
18
|
+
process.exitCode = result.error.code === 'PROVIDER_NOT_FOUND' ? 2 : 3;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
providerId = result.value.provider.id;
|
|
22
|
+
credential = result.value.credential;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const provider = deps.authManager.providerRegistry.get(target);
|
|
26
|
+
if (!provider) {
|
|
27
|
+
process.stderr.write(`Error: No provider found with ID "${target}".\n`);
|
|
28
|
+
process.exitCode = 2;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
providerId = provider.id;
|
|
32
|
+
const result = await deps.authManager.getCredentials(providerId);
|
|
33
|
+
if (!isOk(result)) {
|
|
34
|
+
process.stderr.write(`Error: ${result.error.message}\n`);
|
|
35
|
+
process.exitCode = result.error.code === 'CREDENTIAL_NOT_FOUND' ? 3 : 1;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
credential = result.value;
|
|
39
|
+
}
|
|
40
|
+
const headers = deps.authManager.applyToRequest(providerId, credential);
|
|
41
|
+
const entries = Object.entries(headers);
|
|
42
|
+
if (entries.length === 0) {
|
|
43
|
+
process.stderr.write(`Error: No credential headers produced for "${providerId}".\n`);
|
|
44
|
+
process.exitCode = 3;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const primaryEntry = entries.find(([name]) => PRIMARY_HEADERS.includes(name.toLowerCase()))
|
|
48
|
+
?? entries[0];
|
|
49
|
+
const [primaryHeaderName, primaryHeaderValue] = primaryEntry;
|
|
50
|
+
const xHeaders = {};
|
|
51
|
+
for (const [name, value] of entries) {
|
|
52
|
+
if (name !== primaryHeaderName) {
|
|
53
|
+
xHeaders[name] = value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const format = flags.format ?? 'json';
|
|
57
|
+
switch (format) {
|
|
58
|
+
case 'json': {
|
|
59
|
+
const output = {
|
|
60
|
+
provider: providerId,
|
|
61
|
+
credential: primaryHeaderValue,
|
|
62
|
+
headerName: primaryHeaderName,
|
|
63
|
+
type: credential.type,
|
|
64
|
+
};
|
|
65
|
+
if (Object.keys(xHeaders).length > 0)
|
|
66
|
+
output.xHeaders = xHeaders;
|
|
67
|
+
process.stdout.write(formatJson(output) + '\n');
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case 'header': {
|
|
71
|
+
process.stdout.write(formatCredentialHeaders(headers) + '\n');
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case 'value': {
|
|
75
|
+
process.stdout.write(primaryHeaderValue + '\n');
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
default: {
|
|
79
|
+
process.stderr.write(`Unknown format: ${format}\n`);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sig init — Interactive setup command.
|
|
3
|
+
* Creates ~/.signet/config.yaml with sensible defaults.
|
|
4
|
+
* Does NOT take deps (runs before config exists).
|
|
5
|
+
*/
|
|
6
|
+
export declare function runInit(positionals: string[], flags: Record<string, string | boolean>): Promise<void>;
|