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.

Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +393 -0
  3. package/bin/sig.js +65 -0
  4. package/dist/auth-manager.d.ts +90 -0
  5. package/dist/auth-manager.js +262 -0
  6. package/dist/browser/adapters/playwright.adapter.d.ts +14 -0
  7. package/dist/browser/adapters/playwright.adapter.js +188 -0
  8. package/dist/browser/flows/form-login.flow.d.ts +6 -0
  9. package/dist/browser/flows/form-login.flow.js +35 -0
  10. package/dist/browser/flows/header-capture.d.ts +23 -0
  11. package/dist/browser/flows/header-capture.js +104 -0
  12. package/dist/browser/flows/hybrid-flow.d.ts +37 -0
  13. package/dist/browser/flows/hybrid-flow.js +104 -0
  14. package/dist/browser/flows/oauth-consent.flow.d.ts +20 -0
  15. package/dist/browser/flows/oauth-consent.flow.js +170 -0
  16. package/dist/cli/commands/doctor.d.ts +6 -0
  17. package/dist/cli/commands/doctor.js +263 -0
  18. package/dist/cli/commands/get.d.ts +2 -0
  19. package/dist/cli/commands/get.js +83 -0
  20. package/dist/cli/commands/init.d.ts +6 -0
  21. package/dist/cli/commands/init.js +244 -0
  22. package/dist/cli/commands/login.d.ts +2 -0
  23. package/dist/cli/commands/login.js +77 -0
  24. package/dist/cli/commands/logout.d.ts +2 -0
  25. package/dist/cli/commands/logout.js +11 -0
  26. package/dist/cli/commands/providers.d.ts +2 -0
  27. package/dist/cli/commands/providers.js +30 -0
  28. package/dist/cli/commands/remote.d.ts +1 -0
  29. package/dist/cli/commands/remote.js +67 -0
  30. package/dist/cli/commands/request.d.ts +2 -0
  31. package/dist/cli/commands/request.js +82 -0
  32. package/dist/cli/commands/status.d.ts +2 -0
  33. package/dist/cli/commands/status.js +41 -0
  34. package/dist/cli/commands/sync.d.ts +2 -0
  35. package/dist/cli/commands/sync.js +62 -0
  36. package/dist/cli/formatters.d.ts +3 -0
  37. package/dist/cli/formatters.js +25 -0
  38. package/dist/cli/main.d.ts +8 -0
  39. package/dist/cli/main.js +125 -0
  40. package/dist/config/generator.d.ts +24 -0
  41. package/dist/config/generator.js +97 -0
  42. package/dist/config/loader.d.ts +21 -0
  43. package/dist/config/loader.js +54 -0
  44. package/dist/config/schema.d.ts +44 -0
  45. package/dist/config/schema.js +8 -0
  46. package/dist/config/validator.d.ts +15 -0
  47. package/dist/config/validator.js +228 -0
  48. package/dist/core/errors.d.ts +57 -0
  49. package/dist/core/errors.js +107 -0
  50. package/dist/core/interfaces/auth-strategy.d.ts +48 -0
  51. package/dist/core/interfaces/auth-strategy.js +1 -0
  52. package/dist/core/interfaces/browser-adapter.d.ts +73 -0
  53. package/dist/core/interfaces/browser-adapter.js +1 -0
  54. package/dist/core/interfaces/provider.d.ts +15 -0
  55. package/dist/core/interfaces/provider.js +1 -0
  56. package/dist/core/interfaces/storage.d.ts +21 -0
  57. package/dist/core/interfaces/storage.js +1 -0
  58. package/dist/core/result.d.ts +21 -0
  59. package/dist/core/result.js +16 -0
  60. package/dist/core/types.d.ts +128 -0
  61. package/dist/core/types.js +6 -0
  62. package/dist/deps.d.ts +20 -0
  63. package/dist/deps.js +54 -0
  64. package/dist/index.d.ts +35 -0
  65. package/dist/index.js +37 -0
  66. package/dist/providers/auto-provision.d.ts +9 -0
  67. package/dist/providers/auto-provision.js +27 -0
  68. package/dist/providers/config-loader.d.ts +7 -0
  69. package/dist/providers/config-loader.js +7 -0
  70. package/dist/providers/provider-registry.d.ts +19 -0
  71. package/dist/providers/provider-registry.js +68 -0
  72. package/dist/storage/cached-storage.d.ts +24 -0
  73. package/dist/storage/cached-storage.js +57 -0
  74. package/dist/storage/directory-storage.d.ts +25 -0
  75. package/dist/storage/directory-storage.js +184 -0
  76. package/dist/storage/memory-storage.d.ts +14 -0
  77. package/dist/storage/memory-storage.js +27 -0
  78. package/dist/strategies/api-token.strategy.d.ts +6 -0
  79. package/dist/strategies/api-token.strategy.js +63 -0
  80. package/dist/strategies/basic-auth.strategy.d.ts +6 -0
  81. package/dist/strategies/basic-auth.strategy.js +41 -0
  82. package/dist/strategies/cookie.strategy.d.ts +6 -0
  83. package/dist/strategies/cookie.strategy.js +118 -0
  84. package/dist/strategies/oauth2.strategy.d.ts +6 -0
  85. package/dist/strategies/oauth2.strategy.js +134 -0
  86. package/dist/strategies/registry.d.ts +13 -0
  87. package/dist/strategies/registry.js +25 -0
  88. package/dist/sync/remote-config.d.ts +8 -0
  89. package/dist/sync/remote-config.js +49 -0
  90. package/dist/sync/sync-engine.d.ts +10 -0
  91. package/dist/sync/sync-engine.js +96 -0
  92. package/dist/sync/transports/ssh.d.ts +18 -0
  93. package/dist/sync/transports/ssh.js +115 -0
  94. package/dist/sync/types.d.ts +17 -0
  95. package/dist/sync/types.js +1 -0
  96. package/dist/utils/duration.d.ts +9 -0
  97. package/dist/utils/duration.js +34 -0
  98. package/dist/utils/http.d.ts +4 -0
  99. package/dist/utils/http.js +10 -0
  100. package/dist/utils/jwt.d.ts +15 -0
  101. package/dist/utils/jwt.js +30 -0
  102. package/package.json +56 -0
  103. package/src/auth-manager.ts +331 -0
  104. package/src/browser/adapters/playwright.adapter.ts +247 -0
  105. package/src/browser/flows/form-login.flow.ts +35 -0
  106. package/src/browser/flows/header-capture.ts +128 -0
  107. package/src/browser/flows/hybrid-flow.ts +165 -0
  108. package/src/browser/flows/oauth-consent.flow.ts +200 -0
  109. package/src/cli/commands/doctor.ts +301 -0
  110. package/src/cli/commands/get.ts +96 -0
  111. package/src/cli/commands/init.ts +289 -0
  112. package/src/cli/commands/login.ts +94 -0
  113. package/src/cli/commands/logout.ts +17 -0
  114. package/src/cli/commands/providers.ts +39 -0
  115. package/src/cli/commands/remote.ts +71 -0
  116. package/src/cli/commands/request.ts +97 -0
  117. package/src/cli/commands/status.ts +48 -0
  118. package/src/cli/commands/sync.ts +71 -0
  119. package/src/cli/formatters.ts +31 -0
  120. package/src/cli/main.ts +144 -0
  121. package/src/config/generator.ts +122 -0
  122. package/src/config/loader.ts +70 -0
  123. package/src/config/schema.ts +75 -0
  124. package/src/config/validator.ts +281 -0
  125. package/src/core/errors.ts +182 -0
  126. package/src/core/interfaces/auth-strategy.ts +65 -0
  127. package/src/core/interfaces/browser-adapter.ts +81 -0
  128. package/src/core/interfaces/provider.ts +19 -0
  129. package/src/core/interfaces/storage.ts +26 -0
  130. package/src/core/result.ts +24 -0
  131. package/src/core/types.ts +194 -0
  132. package/src/deps.ts +80 -0
  133. package/src/index.ts +109 -0
  134. package/src/providers/auto-provision.ts +30 -0
  135. package/src/providers/config-loader.ts +8 -0
  136. package/src/providers/provider-registry.ts +79 -0
  137. package/src/storage/cached-storage.ts +72 -0
  138. package/src/storage/directory-storage.ts +204 -0
  139. package/src/storage/memory-storage.ts +35 -0
  140. package/src/strategies/api-token.strategy.ts +87 -0
  141. package/src/strategies/basic-auth.strategy.ts +64 -0
  142. package/src/strategies/cookie.strategy.ts +153 -0
  143. package/src/strategies/oauth2.strategy.ts +178 -0
  144. package/src/strategies/registry.ts +34 -0
  145. package/src/sync/remote-config.ts +60 -0
  146. package/src/sync/sync-engine.ts +113 -0
  147. package/src/sync/transports/ssh.ts +130 -0
  148. package/src/sync/types.ts +15 -0
  149. package/src/utils/duration.ts +34 -0
  150. package/src/utils/http.ts +11 -0
  151. package/src/utils/jwt.ts +39 -0
  152. 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
+ }