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,301 @@
|
|
|
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
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import fsp from 'node:fs/promises';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { execSync } from 'node:child_process';
|
|
12
|
+
import { getConfigPath, loadConfig } from '../../config/loader.js';
|
|
13
|
+
import { isOk } from '../../core/result.js';
|
|
14
|
+
import type { SignetConfig } from '../../config/schema.js';
|
|
15
|
+
|
|
16
|
+
interface CheckResult {
|
|
17
|
+
label: string;
|
|
18
|
+
ok: boolean;
|
|
19
|
+
detail?: string;
|
|
20
|
+
hint?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const PASS = '\u2713'; // ✓
|
|
24
|
+
const FAIL = '\u2717'; // ✗
|
|
25
|
+
|
|
26
|
+
function printResults(results: CheckResult[]): void {
|
|
27
|
+
let failures = 0;
|
|
28
|
+
|
|
29
|
+
for (const r of results) {
|
|
30
|
+
if (r.ok) {
|
|
31
|
+
const detail = r.detail ? ` (${r.detail})` : '';
|
|
32
|
+
console.log(` ${PASS} ${r.label}${detail}`);
|
|
33
|
+
} else {
|
|
34
|
+
failures++;
|
|
35
|
+
console.log(` ${FAIL} ${r.label}`);
|
|
36
|
+
if (r.hint) {
|
|
37
|
+
console.log(` \u2192 ${r.hint}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log('');
|
|
43
|
+
if (failures === 0) {
|
|
44
|
+
console.log('All checks passed.');
|
|
45
|
+
} else {
|
|
46
|
+
console.log(`${failures} issue${failures > 1 ? 's' : ''} found.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Individual checks
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
function checkConfigExists(): CheckResult {
|
|
55
|
+
const configPath = getConfigPath();
|
|
56
|
+
const exists = fs.existsSync(configPath);
|
|
57
|
+
return {
|
|
58
|
+
label: 'Config file exists',
|
|
59
|
+
ok: exists,
|
|
60
|
+
detail: exists ? configPath.replace(os.homedir(), '~') : undefined,
|
|
61
|
+
hint: exists ? undefined : 'Run "sig init" to create ~/.signet/config.yaml',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function checkConfigValid(): Promise<CheckResult & { config?: SignetConfig }> {
|
|
66
|
+
const configResult = await loadConfig();
|
|
67
|
+
if (!isOk(configResult)) {
|
|
68
|
+
return {
|
|
69
|
+
label: 'Config is valid',
|
|
70
|
+
ok: false,
|
|
71
|
+
hint: configResult.error.message,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
label: 'Config is valid',
|
|
76
|
+
ok: true,
|
|
77
|
+
config: configResult.value,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function checkCredentialsDir(config: SignetConfig | undefined): Promise<CheckResult> {
|
|
82
|
+
if (!config) {
|
|
83
|
+
return {
|
|
84
|
+
label: 'Credentials directory exists',
|
|
85
|
+
ok: false,
|
|
86
|
+
hint: 'Fix config first',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const dir = config.storage.credentialsDir.replace(/^~/, os.homedir());
|
|
91
|
+
try {
|
|
92
|
+
await fsp.access(dir, fs.constants.R_OK | fs.constants.W_OK);
|
|
93
|
+
return {
|
|
94
|
+
label: 'Credentials directory exists',
|
|
95
|
+
ok: true,
|
|
96
|
+
detail: config.storage.credentialsDir,
|
|
97
|
+
};
|
|
98
|
+
} catch {
|
|
99
|
+
return {
|
|
100
|
+
label: 'Credentials directory exists',
|
|
101
|
+
ok: false,
|
|
102
|
+
hint: `Directory not found or not writable: ${config.storage.credentialsDir}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function checkBrowserDataDir(config: SignetConfig | undefined): Promise<CheckResult> {
|
|
108
|
+
if (!config) {
|
|
109
|
+
return {
|
|
110
|
+
label: 'Browser data directory exists',
|
|
111
|
+
ok: false,
|
|
112
|
+
hint: 'Fix config first',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const dir = config.browser.browserDataDir.replace(/^~/, os.homedir());
|
|
117
|
+
const exists = fs.existsSync(dir);
|
|
118
|
+
return {
|
|
119
|
+
label: 'Browser data directory exists',
|
|
120
|
+
ok: exists,
|
|
121
|
+
detail: exists ? config.browser.browserDataDir : undefined,
|
|
122
|
+
hint: exists ? undefined : `Directory not found: ${config.browser.browserDataDir}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function findChannelBrowser(channel: string): boolean {
|
|
127
|
+
const platform = process.platform;
|
|
128
|
+
|
|
129
|
+
// Check common browser locations based on channel and platform
|
|
130
|
+
if (platform === 'darwin') {
|
|
131
|
+
const apps: Record<string, string> = {
|
|
132
|
+
chrome: '/Applications/Google Chrome.app',
|
|
133
|
+
msedge: '/Applications/Microsoft Edge.app',
|
|
134
|
+
chromium: '/Applications/Chromium.app',
|
|
135
|
+
};
|
|
136
|
+
if (apps[channel]) return fs.existsSync(apps[channel]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (platform === 'linux') {
|
|
140
|
+
const bins: Record<string, string> = {
|
|
141
|
+
chrome: 'google-chrome',
|
|
142
|
+
msedge: 'microsoft-edge',
|
|
143
|
+
chromium: 'chromium',
|
|
144
|
+
};
|
|
145
|
+
if (bins[channel]) {
|
|
146
|
+
try {
|
|
147
|
+
execSync(`which ${bins[channel]}`, { stdio: 'ignore' });
|
|
148
|
+
return true;
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function checkBrowserAvailable(config: SignetConfig | undefined): Promise<CheckResult> {
|
|
159
|
+
if (!config) {
|
|
160
|
+
return {
|
|
161
|
+
label: 'Browser available',
|
|
162
|
+
ok: false,
|
|
163
|
+
hint: 'Fix config first',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const channel = config.browser.channel;
|
|
168
|
+
try {
|
|
169
|
+
// Verify playwright-core is importable
|
|
170
|
+
await import('playwright-core');
|
|
171
|
+
|
|
172
|
+
// Check if the channel browser is installed on the system
|
|
173
|
+
const found = findChannelBrowser(channel);
|
|
174
|
+
if (found) {
|
|
175
|
+
return {
|
|
176
|
+
label: 'Browser available',
|
|
177
|
+
ok: true,
|
|
178
|
+
detail: channel,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Fallback: if we can't detect by channel but playwright-core is available, report cautiously
|
|
183
|
+
return {
|
|
184
|
+
label: 'Browser available',
|
|
185
|
+
ok: true,
|
|
186
|
+
detail: `${channel} (playwright-core loaded, browser not verified)`,
|
|
187
|
+
};
|
|
188
|
+
} catch {
|
|
189
|
+
return {
|
|
190
|
+
label: 'Browser available',
|
|
191
|
+
ok: false,
|
|
192
|
+
hint: 'playwright-core not installed. Run "npm install playwright-core".',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function checkNodeVersion(): CheckResult {
|
|
198
|
+
const version = process.version;
|
|
199
|
+
const match = version.match(/^v(\d+)/);
|
|
200
|
+
const major = match ? parseInt(match[1], 10) : 0;
|
|
201
|
+
return {
|
|
202
|
+
label: 'Node.js version',
|
|
203
|
+
ok: major >= 18,
|
|
204
|
+
detail: version,
|
|
205
|
+
hint: major < 18 ? `Node.js >= 18 is required. Current: ${version}` : undefined,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function checkStoredCredentials(config: SignetConfig | undefined): Promise<CheckResult> {
|
|
210
|
+
if (!config) {
|
|
211
|
+
return {
|
|
212
|
+
label: 'Stored credentials',
|
|
213
|
+
ok: true,
|
|
214
|
+
detail: 'skipped (no config)',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const dir = config.storage.credentialsDir.replace(/^~/, os.homedir());
|
|
219
|
+
try {
|
|
220
|
+
const files = await fsp.readdir(dir);
|
|
221
|
+
const jsonFiles = files.filter(f => f.endsWith('.json') && !f.endsWith('.lock'));
|
|
222
|
+
const total = jsonFiles.length;
|
|
223
|
+
|
|
224
|
+
// Check for expired credentials by reading each file
|
|
225
|
+
let expired = 0;
|
|
226
|
+
for (const file of jsonFiles) {
|
|
227
|
+
try {
|
|
228
|
+
const content = await fsp.readFile(path.join(dir, file), 'utf-8');
|
|
229
|
+
const data = JSON.parse(content);
|
|
230
|
+
const cred = data?.credential;
|
|
231
|
+
if (cred?.type === 'bearer' && cred?.token) {
|
|
232
|
+
try {
|
|
233
|
+
const { isJwtExpired } = await import('../../utils/jwt.js');
|
|
234
|
+
if (isJwtExpired(cred.token)) expired++;
|
|
235
|
+
} catch {
|
|
236
|
+
// JWT decode failed, skip
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
// Skip unreadable files
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const detail = expired > 0
|
|
245
|
+
? `${total} stored credential${total !== 1 ? 's' : ''} (${expired} expired)`
|
|
246
|
+
: `${total} stored credential${total !== 1 ? 's' : ''}`;
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
label: 'Stored credentials',
|
|
250
|
+
ok: true,
|
|
251
|
+
detail,
|
|
252
|
+
};
|
|
253
|
+
} catch {
|
|
254
|
+
return {
|
|
255
|
+
label: 'Stored credentials',
|
|
256
|
+
ok: true,
|
|
257
|
+
detail: '0 stored credentials',
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// Main command
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
export async function runDoctor(
|
|
267
|
+
positionals: string[],
|
|
268
|
+
flags: Record<string, string | boolean>,
|
|
269
|
+
): Promise<void> {
|
|
270
|
+
const results: CheckResult[] = [];
|
|
271
|
+
|
|
272
|
+
// a. Config file exists
|
|
273
|
+
results.push(checkConfigExists());
|
|
274
|
+
|
|
275
|
+
// b. Config is valid
|
|
276
|
+
const configCheck = await checkConfigValid();
|
|
277
|
+
results.push(configCheck);
|
|
278
|
+
const config = configCheck.config;
|
|
279
|
+
|
|
280
|
+
// c. Credentials directory
|
|
281
|
+
results.push(await checkCredentialsDir(config));
|
|
282
|
+
|
|
283
|
+
// d. Browser data directory
|
|
284
|
+
results.push(await checkBrowserDataDir(config));
|
|
285
|
+
|
|
286
|
+
// e. Browser available
|
|
287
|
+
results.push(await checkBrowserAvailable(config));
|
|
288
|
+
|
|
289
|
+
// f. Node.js version
|
|
290
|
+
results.push(checkNodeVersion());
|
|
291
|
+
|
|
292
|
+
// g. Stored credentials
|
|
293
|
+
results.push(await checkStoredCredentials(config));
|
|
294
|
+
|
|
295
|
+
printResults(results);
|
|
296
|
+
|
|
297
|
+
const hasFailures = results.some(r => !r.ok);
|
|
298
|
+
if (hasFailures) {
|
|
299
|
+
process.exitCode = 1;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { AuthDeps } from '../../deps.js';
|
|
2
|
+
import { isOk } from '../../core/result.js';
|
|
3
|
+
import { formatJson, formatCredentialHeaders } from '../formatters.js';
|
|
4
|
+
|
|
5
|
+
const PRIMARY_HEADERS = ['cookie', 'authorization'];
|
|
6
|
+
|
|
7
|
+
export async function runGet(
|
|
8
|
+
positionals: string[],
|
|
9
|
+
flags: Record<string, string | boolean>,
|
|
10
|
+
deps: AuthDeps,
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
const target = positionals[0];
|
|
13
|
+
if (!target) {
|
|
14
|
+
process.stderr.write('Usage: sig get <provider|url>\n');
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isUrl = target.startsWith('http://') || target.startsWith('https://');
|
|
20
|
+
let providerId: string;
|
|
21
|
+
let credential;
|
|
22
|
+
|
|
23
|
+
if (isUrl) {
|
|
24
|
+
const result = await deps.authManager.getCredentialsByUrl(target);
|
|
25
|
+
if (!isOk(result)) {
|
|
26
|
+
process.stderr.write(`Error: ${result.error.message}\n`);
|
|
27
|
+
process.exitCode = result.error.code === 'PROVIDER_NOT_FOUND' ? 2 : 3;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
providerId = result.value.provider.id;
|
|
31
|
+
credential = result.value.credential;
|
|
32
|
+
} else {
|
|
33
|
+
const provider = deps.authManager.providerRegistry.get(target);
|
|
34
|
+
if (!provider) {
|
|
35
|
+
process.stderr.write(`Error: No provider found with ID "${target}".\n`);
|
|
36
|
+
process.exitCode = 2;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
providerId = provider.id;
|
|
40
|
+
const result = await deps.authManager.getCredentials(providerId);
|
|
41
|
+
if (!isOk(result)) {
|
|
42
|
+
process.stderr.write(`Error: ${result.error.message}\n`);
|
|
43
|
+
process.exitCode = result.error.code === 'CREDENTIAL_NOT_FOUND' ? 3 : 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
credential = result.value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const headers = deps.authManager.applyToRequest(providerId, credential);
|
|
50
|
+
const entries = Object.entries(headers);
|
|
51
|
+
|
|
52
|
+
if (entries.length === 0) {
|
|
53
|
+
process.stderr.write(`Error: No credential headers produced for "${providerId}".\n`);
|
|
54
|
+
process.exitCode = 3;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const primaryEntry = entries.find(([name]) => PRIMARY_HEADERS.includes(name.toLowerCase()))
|
|
59
|
+
?? entries[0];
|
|
60
|
+
const [primaryHeaderName, primaryHeaderValue] = primaryEntry;
|
|
61
|
+
|
|
62
|
+
const xHeaders: Record<string, string> = {};
|
|
63
|
+
for (const [name, value] of entries) {
|
|
64
|
+
if (name !== primaryHeaderName) {
|
|
65
|
+
xHeaders[name] = value;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const format = (flags.format as string) ?? 'json';
|
|
70
|
+
|
|
71
|
+
switch (format) {
|
|
72
|
+
case 'json': {
|
|
73
|
+
const output: Record<string, unknown> = {
|
|
74
|
+
provider: providerId,
|
|
75
|
+
credential: primaryHeaderValue,
|
|
76
|
+
headerName: primaryHeaderName,
|
|
77
|
+
type: credential.type,
|
|
78
|
+
};
|
|
79
|
+
if (Object.keys(xHeaders).length > 0) output.xHeaders = xHeaders;
|
|
80
|
+
process.stdout.write(formatJson(output) + '\n');
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case 'header': {
|
|
84
|
+
process.stdout.write(formatCredentialHeaders(headers) + '\n');
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case 'value': {
|
|
88
|
+
process.stdout.write(primaryHeaderValue + '\n');
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
default: {
|
|
92
|
+
process.stderr.write(`Unknown format: ${format}\n`);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
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
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import fsp from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import { execSync } from 'node:child_process';
|
|
12
|
+
import { createInterface } from 'node:readline/promises';
|
|
13
|
+
import YAML from 'yaml';
|
|
14
|
+
import { getConfigPath } from '../../config/loader.js';
|
|
15
|
+
import { generateConfigYaml } from '../../config/generator.js';
|
|
16
|
+
import { validateConfig } from '../../config/validator.js';
|
|
17
|
+
import { isOk } from '../../core/result.js';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Provider templates
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
interface ProviderTemplate {
|
|
24
|
+
name: string;
|
|
25
|
+
domains: string[];
|
|
26
|
+
strategy: string;
|
|
27
|
+
config?: Record<string, unknown>;
|
|
28
|
+
needsDomain?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const PROVIDER_TEMPLATES: Record<string, ProviderTemplate> = {
|
|
32
|
+
github: {
|
|
33
|
+
name: 'GitHub',
|
|
34
|
+
domains: ['github.com', 'api.github.com'],
|
|
35
|
+
strategy: 'api-token',
|
|
36
|
+
config: { headerName: 'Authorization', headerPrefix: 'Bearer' },
|
|
37
|
+
},
|
|
38
|
+
gitlab: {
|
|
39
|
+
name: 'GitLab',
|
|
40
|
+
domains: ['gitlab.com'],
|
|
41
|
+
strategy: 'api-token',
|
|
42
|
+
config: { headerName: 'PRIVATE-TOKEN', headerPrefix: '' },
|
|
43
|
+
},
|
|
44
|
+
jira: {
|
|
45
|
+
name: 'Jira (Cloud)',
|
|
46
|
+
domains: [],
|
|
47
|
+
strategy: 'cookie',
|
|
48
|
+
needsDomain: true,
|
|
49
|
+
},
|
|
50
|
+
confluence: {
|
|
51
|
+
name: 'Confluence',
|
|
52
|
+
domains: [],
|
|
53
|
+
strategy: 'cookie',
|
|
54
|
+
needsDomain: true,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Browser channel detection
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
function detectBrowserChannel(): string {
|
|
63
|
+
const platform = process.platform;
|
|
64
|
+
|
|
65
|
+
if (platform === 'darwin') {
|
|
66
|
+
if (fs.existsSync('/Applications/Google Chrome.app')) return 'chrome';
|
|
67
|
+
if (fs.existsSync('/Applications/Microsoft Edge.app')) return 'msedge';
|
|
68
|
+
return 'chrome';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (platform === 'linux') {
|
|
72
|
+
try { execSync('which google-chrome', { stdio: 'ignore' }); return 'chrome'; } catch { /* not found */ }
|
|
73
|
+
try { execSync('which microsoft-edge', { stdio: 'ignore' }); return 'msedge'; } catch { /* not found */ }
|
|
74
|
+
try { execSync('which chromium', { stdio: 'ignore' }); return 'chromium'; } catch { /* not found */ }
|
|
75
|
+
return 'chrome';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Windows or unknown
|
|
79
|
+
return 'chrome';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Interactive prompts
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
async function promptProviders(rl: ReturnType<typeof createInterface>): Promise<Array<{
|
|
87
|
+
id: string;
|
|
88
|
+
domains: string[];
|
|
89
|
+
strategy: string;
|
|
90
|
+
entryUrl?: string;
|
|
91
|
+
config?: Record<string, unknown>;
|
|
92
|
+
}>> {
|
|
93
|
+
const providers: Array<{
|
|
94
|
+
id: string;
|
|
95
|
+
domains: string[];
|
|
96
|
+
strategy: string;
|
|
97
|
+
entryUrl?: string;
|
|
98
|
+
config?: Record<string, unknown>;
|
|
99
|
+
}> = [];
|
|
100
|
+
|
|
101
|
+
const addMore = await rl.question('\nWould you like to add a provider? (y/N) ');
|
|
102
|
+
if (addMore.toLowerCase() !== 'y') return providers;
|
|
103
|
+
|
|
104
|
+
let keepAdding = true;
|
|
105
|
+
while (keepAdding) {
|
|
106
|
+
const templateNames = Object.keys(PROVIDER_TEMPLATES);
|
|
107
|
+
console.log('\nProvider templates:');
|
|
108
|
+
for (let i = 0; i < templateNames.length; i++) {
|
|
109
|
+
const key = templateNames[i];
|
|
110
|
+
const tmpl = PROVIDER_TEMPLATES[key];
|
|
111
|
+
console.log(` ${i + 1}. ${tmpl.name} (${key})`);
|
|
112
|
+
}
|
|
113
|
+
console.log(` ${templateNames.length + 1}. Custom`);
|
|
114
|
+
|
|
115
|
+
const choice = await rl.question(`\nSelect template (1-${templateNames.length + 1}): `);
|
|
116
|
+
const choiceNum = parseInt(choice, 10);
|
|
117
|
+
|
|
118
|
+
if (choiceNum >= 1 && choiceNum <= templateNames.length) {
|
|
119
|
+
const templateKey = templateNames[choiceNum - 1];
|
|
120
|
+
const template = PROVIDER_TEMPLATES[templateKey];
|
|
121
|
+
|
|
122
|
+
let domains = template.domains;
|
|
123
|
+
if (template.needsDomain) {
|
|
124
|
+
const domain = await rl.question(`Enter your ${template.name} domain (e.g., ${templateKey}.example.com): `);
|
|
125
|
+
if (domain.trim()) {
|
|
126
|
+
domains = [domain.trim()];
|
|
127
|
+
} else {
|
|
128
|
+
console.log(' Skipping — domain is required.');
|
|
129
|
+
const again = await rl.question('\nAdd another provider? (y/N) ');
|
|
130
|
+
keepAdding = again.toLowerCase() === 'y';
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
providers.push({
|
|
136
|
+
id: templateKey,
|
|
137
|
+
domains,
|
|
138
|
+
strategy: template.strategy,
|
|
139
|
+
...(template.config ? { config: template.config } : {}),
|
|
140
|
+
});
|
|
141
|
+
console.log(` Added ${template.name}.`);
|
|
142
|
+
} else if (choiceNum === templateNames.length + 1) {
|
|
143
|
+
const id = await rl.question('Provider id (e.g., my-api): ');
|
|
144
|
+
if (!id.trim()) {
|
|
145
|
+
console.log(' Skipping — id is required.');
|
|
146
|
+
const again = await rl.question('\nAdd another provider? (y/N) ');
|
|
147
|
+
keepAdding = again.toLowerCase() === 'y';
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const domain = await rl.question('Domain(s) (comma-separated): ');
|
|
151
|
+
const domains = domain.split(',').map(d => d.trim()).filter(Boolean);
|
|
152
|
+
if (domains.length === 0) {
|
|
153
|
+
console.log(' Skipping — at least one domain is required.');
|
|
154
|
+
const again = await rl.question('\nAdd another provider? (y/N) ');
|
|
155
|
+
keepAdding = again.toLowerCase() === 'y';
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const strategy = await rl.question('Strategy (cookie, oauth2, api-token, basic) [cookie]: ') || 'cookie';
|
|
159
|
+
providers.push({ id: id.trim(), domains, strategy });
|
|
160
|
+
console.log(` Added custom provider "${id.trim()}".`);
|
|
161
|
+
} else {
|
|
162
|
+
console.log(' Invalid selection.');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const again = await rl.question('\nAdd another provider? (y/N) ');
|
|
166
|
+
keepAdding = again.toLowerCase() === 'y';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return providers;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Main command
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
export async function runInit(
|
|
177
|
+
positionals: string[],
|
|
178
|
+
flags: Record<string, string | boolean>,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
const configPath = getConfigPath();
|
|
181
|
+
const signetDir = path.dirname(configPath);
|
|
182
|
+
const force = flags.force === true;
|
|
183
|
+
const yes = flags.yes === true;
|
|
184
|
+
|
|
185
|
+
// Check if config already exists
|
|
186
|
+
if (fs.existsSync(configPath) && !force) {
|
|
187
|
+
process.stderr.write(
|
|
188
|
+
`Config file already exists: ${configPath}\n` +
|
|
189
|
+
'Use --force to overwrite.\n',
|
|
190
|
+
);
|
|
191
|
+
process.exitCode = 1;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Detect defaults
|
|
196
|
+
const detectedChannel = detectBrowserChannel();
|
|
197
|
+
const defaultChannel = typeof flags.channel === 'string' ? flags.channel : detectedChannel;
|
|
198
|
+
const defaultBrowserDataDir = typeof flags['browser-data-dir'] === 'string'
|
|
199
|
+
? flags['browser-data-dir']
|
|
200
|
+
: path.join(signetDir, 'browser-data');
|
|
201
|
+
const defaultCredentialsDir = typeof flags['credentials-dir'] === 'string'
|
|
202
|
+
? flags['credentials-dir']
|
|
203
|
+
: path.join(signetDir, 'credentials');
|
|
204
|
+
|
|
205
|
+
let channel = defaultChannel;
|
|
206
|
+
let browserDataDir = defaultBrowserDataDir;
|
|
207
|
+
let credentialsDir = defaultCredentialsDir;
|
|
208
|
+
let providers: Array<{
|
|
209
|
+
id: string;
|
|
210
|
+
domains: string[];
|
|
211
|
+
strategy: string;
|
|
212
|
+
entryUrl?: string;
|
|
213
|
+
config?: Record<string, unknown>;
|
|
214
|
+
}> = [];
|
|
215
|
+
|
|
216
|
+
// Interactive mode
|
|
217
|
+
const isTTY = process.stdin.isTTY && process.stdout.isTTY;
|
|
218
|
+
if (isTTY && !yes) {
|
|
219
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
220
|
+
try {
|
|
221
|
+
console.log('\nWelcome to Signet! Let\'s set up your configuration.\n');
|
|
222
|
+
|
|
223
|
+
const channelAnswer = await rl.question(`Browser channel [${defaultChannel}]: `);
|
|
224
|
+
if (channelAnswer.trim()) channel = channelAnswer.trim();
|
|
225
|
+
|
|
226
|
+
providers = await promptProviders(rl);
|
|
227
|
+
} finally {
|
|
228
|
+
rl.close();
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
if (!yes) {
|
|
232
|
+
// Non-TTY, non-yes: use defaults silently
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Resolve ~ in paths for display but keep ~ in config for portability
|
|
237
|
+
const displayBrowserDataDir = browserDataDir.replace(os.homedir(), '~');
|
|
238
|
+
const displayCredentialsDir = credentialsDir.replace(os.homedir(), '~');
|
|
239
|
+
|
|
240
|
+
// Generate config YAML
|
|
241
|
+
const yaml = generateConfigYaml({
|
|
242
|
+
channel,
|
|
243
|
+
browserDataDir: displayBrowserDataDir,
|
|
244
|
+
credentialsDir: displayCredentialsDir,
|
|
245
|
+
headlessTimeout: 30_000,
|
|
246
|
+
visibleTimeout: 120_000,
|
|
247
|
+
waitUntil: 'load',
|
|
248
|
+
providers: providers.length > 0 ? providers : undefined,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Validate the generated config before writing (sanity check)
|
|
252
|
+
let raw: unknown;
|
|
253
|
+
try {
|
|
254
|
+
raw = YAML.parse(yaml);
|
|
255
|
+
} catch (e: unknown) {
|
|
256
|
+
process.stderr.write(`Bug: generated invalid YAML: ${(e as Error).message}\n`);
|
|
257
|
+
process.exitCode = 1;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const validationResult = validateConfig(raw as Record<string, unknown>);
|
|
262
|
+
if (!isOk(validationResult)) {
|
|
263
|
+
process.stderr.write(`Bug: generated config failed validation: ${validationResult.error.message}\n`);
|
|
264
|
+
process.exitCode = 1;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Create directories
|
|
269
|
+
await fsp.mkdir(signetDir, { recursive: true });
|
|
270
|
+
await fsp.mkdir(browserDataDir, { recursive: true });
|
|
271
|
+
await fsp.mkdir(credentialsDir, { recursive: true });
|
|
272
|
+
|
|
273
|
+
// Write config
|
|
274
|
+
await fsp.writeFile(configPath, yaml, 'utf-8');
|
|
275
|
+
|
|
276
|
+
// Success message
|
|
277
|
+
console.log(`\n Config written to ${configPath}`);
|
|
278
|
+
console.log(` Browser data: ${browserDataDir}`);
|
|
279
|
+
console.log(` Credentials: ${credentialsDir}`);
|
|
280
|
+
console.log(` Browser: ${channel}`);
|
|
281
|
+
if (providers.length > 0) {
|
|
282
|
+
console.log(` Providers: ${providers.map(p => p.id).join(', ')}`);
|
|
283
|
+
}
|
|
284
|
+
console.log('\nNext steps:');
|
|
285
|
+
console.log(' sig login <url> Authenticate with a service');
|
|
286
|
+
console.log(' sig providers List configured providers');
|
|
287
|
+
console.log(' sig doctor Check your setup');
|
|
288
|
+
console.log('');
|
|
289
|
+
}
|