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,25 @@
1
+ export function formatJson(data) {
2
+ return JSON.stringify(data, null, 2);
3
+ }
4
+ export function formatTable(rows) {
5
+ if (rows.length === 0)
6
+ return '';
7
+ const columns = Object.keys(rows[0]);
8
+ const widths = new Map();
9
+ for (const col of columns) {
10
+ let max = col.length;
11
+ for (const row of rows) {
12
+ const len = (row[col] ?? '').length;
13
+ if (len > max)
14
+ max = len;
15
+ }
16
+ widths.set(col, max);
17
+ }
18
+ const header = columns.map(c => c.toUpperCase().padEnd(widths.get(c))).join(' ');
19
+ const separator = columns.map(c => '-'.repeat(widths.get(c))).join(' ');
20
+ const body = rows.map(row => columns.map(c => (row[c] ?? '').padEnd(widths.get(c))).join(' '));
21
+ return [header, separator, ...body].join('\n');
22
+ }
23
+ export function formatCredentialHeaders(headers) {
24
+ return Object.entries(headers).map(([k, v]) => `${k}: ${v}`).join('\n');
25
+ }
@@ -0,0 +1,8 @@
1
+ interface ParsedArgs {
2
+ command: string;
3
+ positionals: string[];
4
+ flags: Record<string, string | boolean>;
5
+ }
6
+ export declare function parseArgs(args: string[]): ParsedArgs;
7
+ export declare function run(args: string[]): Promise<void>;
8
+ export {};
@@ -0,0 +1,125 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { loadConfig, getConfigPath } from '../config/loader.js';
3
+ import { createAuthDeps } from '../deps.js';
4
+ import { isOk } from '../core/result.js';
5
+ import { runGet } from './commands/get.js';
6
+ import { runLogin } from './commands/login.js';
7
+ import { runStatus } from './commands/status.js';
8
+ import { runLogout } from './commands/logout.js';
9
+ import { runProviders } from './commands/providers.js';
10
+ import { runRequest } from './commands/request.js';
11
+ import { runRemote } from './commands/remote.js';
12
+ import { runSync } from './commands/sync.js';
13
+ import { runInit } from './commands/init.js';
14
+ import { runDoctor } from './commands/doctor.js';
15
+ export function parseArgs(args) {
16
+ const firstIsFlag = args[0]?.startsWith('--');
17
+ const command = firstIsFlag ? 'help' : (args[0] ?? 'help');
18
+ const positionals = [];
19
+ const flags = {};
20
+ let i = firstIsFlag ? 0 : 1;
21
+ while (i < args.length) {
22
+ const arg = args[i];
23
+ if (arg.startsWith('--')) {
24
+ const key = arg.slice(2);
25
+ const next = args[i + 1];
26
+ if (next !== undefined && !next.startsWith('--')) {
27
+ flags[key] = next;
28
+ i += 2;
29
+ }
30
+ else {
31
+ flags[key] = true;
32
+ i += 1;
33
+ }
34
+ }
35
+ else {
36
+ positionals.push(arg);
37
+ i += 1;
38
+ }
39
+ }
40
+ return { command, positionals, flags };
41
+ }
42
+ const HELP = `Usage: sig <command> [options]
43
+
44
+ Commands:
45
+ init Set up Signet configuration (interactive)
46
+ get <provider|url> Get credential headers for a provider or URL
47
+ login <url> Authenticate with a system (browser or token)
48
+ request <url> Make an authenticated HTTP request
49
+ status [provider] Show authentication status
50
+ logout [provider] Clear stored credentials
51
+ providers List configured providers
52
+ remote Manage remote credential stores
53
+ sync Sync credentials with a remote
54
+ doctor Check environment and configuration
55
+
56
+ Global options:
57
+ --format <json|table|header|value|body> Output format
58
+ --help Show this help message
59
+ `;
60
+ const DEPS_COMMANDS = new Set(['get', 'login', 'status', 'logout', 'providers', 'request', 'sync']);
61
+ export async function run(args) {
62
+ const { command, positionals, flags } = parseArgs(args);
63
+ if (command === 'help' || flags.help === true) {
64
+ process.stdout.write(HELP);
65
+ return;
66
+ }
67
+ // Commands that don't need deps (run before config exists)
68
+ if (command === 'init') {
69
+ await runInit(positionals, flags);
70
+ return;
71
+ }
72
+ if (command === 'doctor') {
73
+ await runDoctor(positionals, flags);
74
+ return;
75
+ }
76
+ let deps;
77
+ if (DEPS_COMMANDS.has(command)) {
78
+ // First-run detection: check if config file exists before loading
79
+ const configPath = getConfigPath();
80
+ if (!existsSync(configPath)) {
81
+ process.stderr.write('\nWelcome to Signet!\n\n' +
82
+ ` No config file found at ${configPath}\n` +
83
+ ' Run "sig init" to set up your configuration.\n\n');
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+ const configResult = await loadConfig();
88
+ if (!isOk(configResult)) {
89
+ process.stderr.write(`Config error: ${configResult.error.message}\n`);
90
+ process.exitCode = 1;
91
+ return;
92
+ }
93
+ deps = createAuthDeps(configResult.value);
94
+ }
95
+ switch (command) {
96
+ case 'get':
97
+ await runGet(positionals, flags, deps);
98
+ break;
99
+ case 'login':
100
+ await runLogin(positionals, flags, deps);
101
+ break;
102
+ case 'request':
103
+ await runRequest(positionals, flags, deps);
104
+ break;
105
+ case 'status':
106
+ await runStatus(positionals, flags, deps);
107
+ break;
108
+ case 'logout':
109
+ await runLogout(positionals, flags, deps);
110
+ break;
111
+ case 'providers':
112
+ await runProviders(positionals, flags, deps);
113
+ break;
114
+ case 'remote':
115
+ await runRemote(positionals, flags);
116
+ break;
117
+ case 'sync':
118
+ await runSync(positionals, flags, deps);
119
+ break;
120
+ default:
121
+ process.stderr.write(`Unknown command: ${command}\n\n`);
122
+ process.stdout.write(HELP);
123
+ process.exitCode = 1;
124
+ }
125
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Config YAML generator for signet.
3
+ * Uses template literals (NOT YAML.stringify) to preserve comments.
4
+ */
5
+ export interface InitOptions {
6
+ channel: string;
7
+ browserDataDir: string;
8
+ credentialsDir: string;
9
+ headlessTimeout: number;
10
+ visibleTimeout: number;
11
+ waitUntil: string;
12
+ providers?: Array<{
13
+ id: string;
14
+ domains: string[];
15
+ strategy: string;
16
+ entryUrl?: string;
17
+ config?: Record<string, unknown>;
18
+ }>;
19
+ }
20
+ /**
21
+ * Generate a commented YAML config string from the given options.
22
+ * Uses template literals to preserve comments and formatting.
23
+ */
24
+ export declare function generateConfigYaml(options: InitOptions): string;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Config YAML generator for signet.
3
+ * Uses template literals (NOT YAML.stringify) to preserve comments.
4
+ */
5
+ function yamlArray(items) {
6
+ return `[${items.map(d => `"${d}"`).join(', ')}]`;
7
+ }
8
+ function renderProviderConfig(config, indent) {
9
+ const spaces = ' '.repeat(indent);
10
+ const lines = [];
11
+ for (const [key, value] of Object.entries(config)) {
12
+ if (Array.isArray(value)) {
13
+ lines.push(`${spaces}${key}: ${yamlArray(value)}`);
14
+ }
15
+ else if (typeof value === 'string') {
16
+ lines.push(`${spaces}${key}: ${value}`);
17
+ }
18
+ else if (typeof value === 'number' || typeof value === 'boolean') {
19
+ lines.push(`${spaces}${key}: ${String(value)}`);
20
+ }
21
+ }
22
+ return lines.join('\n');
23
+ }
24
+ function renderProvider(provider) {
25
+ const lines = [];
26
+ lines.push(` ${provider.id}:`);
27
+ lines.push(` domains: ${yamlArray(provider.domains)}`);
28
+ lines.push(` strategy: ${provider.strategy}`);
29
+ if (provider.entryUrl) {
30
+ lines.push(` entryUrl: ${provider.entryUrl}`);
31
+ }
32
+ if (provider.config && Object.keys(provider.config).length > 0) {
33
+ lines.push(' config:');
34
+ lines.push(renderProviderConfig(provider.config, 6));
35
+ }
36
+ return lines.join('\n');
37
+ }
38
+ /**
39
+ * Generate a commented YAML config string from the given options.
40
+ * Uses template literals to preserve comments and formatting.
41
+ */
42
+ export function generateConfigYaml(options) {
43
+ const providerSection = options.providers && options.providers.length > 0
44
+ ? options.providers.map(p => renderProvider(p)).join('\n\n')
45
+ : ` # No providers configured yet.
46
+ # Add providers here or use "sig login <url>" for auto-provisioning.
47
+ #
48
+ # Example — cookie-based (SSO):
49
+ # my-jira:
50
+ # domains: ["jira.example.com"]
51
+ # strategy: cookie
52
+ # config:
53
+ # ttl: "12h"
54
+ #
55
+ # Example — API token:
56
+ # github:
57
+ # domains: ["github.com", "api.github.com"]
58
+ # strategy: api-token
59
+ # config:
60
+ # headerName: Authorization
61
+ # headerPrefix: Bearer`;
62
+ return `# Signet unified configuration
63
+ # Generated by "sig init". All configuration lives in this single file.
64
+ # Documentation: https://github.com/pylon/signet
65
+
66
+ # Browser settings (required)
67
+ # Controls browser automation for cookie and OAuth2 authentication.
68
+ browser:
69
+ browserDataDir: ${options.browserDataDir} # Persistent browser profile directory
70
+ channel: ${options.channel} # Browser channel (chrome, msedge, chromium)
71
+ headlessTimeout: ${options.headlessTimeout} # Timeout for headless auth attempt (ms)
72
+ visibleTimeout: ${options.visibleTimeout} # Timeout for visible/user-assisted auth (ms)
73
+ waitUntil: ${options.waitUntil} # Page load condition (load, networkidle, domcontentloaded, commit)
74
+
75
+ # Storage settings (required)
76
+ # Where per-provider credential files are stored.
77
+ storage:
78
+ credentialsDir: ${options.credentialsDir} # Per-provider credential files directory
79
+
80
+ # Remote credential stores (optional)
81
+ # Sync credentials to/from remote machines via SSH.
82
+ # remotes:
83
+ # dev-server:
84
+ # type: ssh
85
+ # host: dev.example.com
86
+ # user: deploy
87
+ # path: ~/.signet/credentials
88
+ # sshKey: ~/.ssh/id_ed25519
89
+
90
+ # Provider configurations
91
+ # Cookie-based providers (most SSO systems) don't need config at all —
92
+ # just call "sig login https://jira.example.com" and it auto-provisions.
93
+ # Only create entries here for OAuth2, API tokens, or advanced settings.
94
+ providers:
95
+ ${providerSection}
96
+ `;
97
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Single config file loader for signet.
3
+ * Reads ONLY ~/.signet/config.yaml — no cascade, no env vars.
4
+ */
5
+ import type { Result } from '../core/result.js';
6
+ import { type AuthError } from '../core/errors.js';
7
+ import type { SignetConfig } from './schema.js';
8
+ /**
9
+ * Load and validate the unified config from ~/.signet/config.yaml.
10
+ * Returns Result<SignetConfig, AuthError>.
11
+ */
12
+ export declare function loadConfig(): Promise<Result<SignetConfig, AuthError>>;
13
+ /**
14
+ * Save the full config back to ~/.signet/config.yaml.
15
+ * Used by remote add/remove commands to persist changes.
16
+ */
17
+ export declare function saveConfig(config: SignetConfig): Promise<void>;
18
+ /**
19
+ * Get the config file path (for error messages).
20
+ */
21
+ export declare function getConfigPath(): string;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Single config file loader for signet.
3
+ * Reads ONLY ~/.signet/config.yaml — no cascade, no env vars.
4
+ */
5
+ import fs from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+ import YAML from 'yaml';
9
+ import { err } from '../core/result.js';
10
+ import { ConfigError } from '../core/errors.js';
11
+ import { validateConfig } from './validator.js';
12
+ const CONFIG_PATH = path.join(os.homedir(), '.signet', 'config.yaml');
13
+ /**
14
+ * Load and validate the unified config from ~/.signet/config.yaml.
15
+ * Returns Result<SignetConfig, AuthError>.
16
+ */
17
+ export async function loadConfig() {
18
+ let content;
19
+ try {
20
+ content = await fs.readFile(CONFIG_PATH, 'utf-8');
21
+ }
22
+ catch (e) {
23
+ if (e.code === 'ENOENT') {
24
+ return err(new ConfigError(`Config file not found: ${CONFIG_PATH}. ` +
25
+ 'Create it with browser.browserDataDir, storage.credentialsDir, and providers sections.'));
26
+ }
27
+ return err(new ConfigError(`Failed to read config from ${CONFIG_PATH}: ${e.message}`));
28
+ }
29
+ let raw;
30
+ try {
31
+ raw = YAML.parse(content);
32
+ }
33
+ catch (e) {
34
+ return err(new ConfigError(`Invalid YAML in ${CONFIG_PATH}: ${e.message}`));
35
+ }
36
+ if (!raw || typeof raw !== 'object') {
37
+ return err(new ConfigError(`Config file ${CONFIG_PATH} is empty or not an object.`));
38
+ }
39
+ return validateConfig(raw);
40
+ }
41
+ /**
42
+ * Save the full config back to ~/.signet/config.yaml.
43
+ * Used by remote add/remove commands to persist changes.
44
+ */
45
+ export async function saveConfig(config) {
46
+ await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
47
+ await fs.writeFile(CONFIG_PATH, YAML.stringify(config), 'utf-8');
48
+ }
49
+ /**
50
+ * Get the config file path (for error messages).
51
+ */
52
+ export function getConfigPath() {
53
+ return CONFIG_PATH;
54
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Unified configuration schema for signet.
3
+ * All config lives in ~/.signet/config.yaml — no cascade, no env vars.
4
+ *
5
+ * Strategy config types are defined in core/types.ts (shared vocabulary)
6
+ * and re-exported here for convenience.
7
+ */
8
+ import type { CredentialType, XHeaderConfig, StrategyName } from '../core/types.js';
9
+ export type { CookieStrategyConfig, OAuth2StrategyConfig, ApiTokenStrategyConfig, BasicStrategyConfig, StrategyConfig, StrategyName, } from '../core/types.js';
10
+ export interface BrowserConfig {
11
+ browserDataDir: string;
12
+ channel: string;
13
+ headlessTimeout: number;
14
+ visibleTimeout: number;
15
+ waitUntil: 'load' | 'networkidle' | 'domcontentloaded' | 'commit';
16
+ }
17
+ export interface StorageConfig {
18
+ credentialsDir: string;
19
+ }
20
+ export interface RemoteEntry {
21
+ type: 'ssh';
22
+ host: string;
23
+ user?: string;
24
+ path?: string;
25
+ sshKey?: string;
26
+ }
27
+ export interface SignetConfig {
28
+ browser: BrowserConfig;
29
+ storage: StorageConfig;
30
+ remotes?: Record<string, RemoteEntry>;
31
+ providers: Record<string, ProviderEntry>;
32
+ }
33
+ export interface ProviderEntry {
34
+ name?: string;
35
+ domains: string[];
36
+ entryUrl?: string;
37
+ strategy: StrategyName;
38
+ config?: Record<string, unknown>;
39
+ acceptedCredentialTypes?: CredentialType[];
40
+ setupInstructions?: string;
41
+ credentialFile?: string;
42
+ xHeaders?: XHeaderConfig[];
43
+ forceVisible?: boolean;
44
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Unified configuration schema for signet.
3
+ * All config lives in ~/.signet/config.yaml — no cascade, no env vars.
4
+ *
5
+ * Strategy config types are defined in core/types.ts (shared vocabulary)
6
+ * and re-exported here for convenience.
7
+ */
8
+ export {};
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Runtime validation for the unified signet config.
3
+ * Returns Result<SignetConfig, AuthError>.
4
+ */
5
+ import type { Result } from '../core/result.js';
6
+ import { type AuthError } from '../core/errors.js';
7
+ import type { SignetConfig, StrategyName, StrategyConfig } from './schema.js';
8
+ /**
9
+ * Validate a raw config object parsed from YAML.
10
+ */
11
+ export declare function validateConfig(raw: Record<string, unknown>): Result<SignetConfig, AuthError>;
12
+ /**
13
+ * Merge a provider entry's strategy + config into a typed StrategyConfig.
14
+ */
15
+ export declare function buildStrategyConfig(strategy: StrategyName, config?: Record<string, unknown>): StrategyConfig;
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Runtime validation for the unified signet config.
3
+ * Returns Result<SignetConfig, AuthError>.
4
+ */
5
+ import { ok, err } from '../core/result.js';
6
+ import { ConfigError } from '../core/errors.js';
7
+ const VALID_STRATEGIES = ['cookie', 'oauth2', 'api-token', 'basic'];
8
+ const VALID_WAIT_UNTIL = ['load', 'networkidle', 'domcontentloaded', 'commit'];
9
+ /**
10
+ * Validate a raw config object parsed from YAML.
11
+ */
12
+ export function validateConfig(raw) {
13
+ const errors = [];
14
+ // --- browser section ---
15
+ if (!raw.browser || typeof raw.browser !== 'object') {
16
+ errors.push('Missing required section: "browser"');
17
+ }
18
+ else {
19
+ const browser = raw.browser;
20
+ if (typeof browser.browserDataDir !== 'string' || browser.browserDataDir.trim() === '') {
21
+ errors.push('Missing required field: browser.browserDataDir');
22
+ }
23
+ if (typeof browser.channel !== 'string' || browser.channel.trim() === '') {
24
+ errors.push('Missing required field: browser.channel');
25
+ }
26
+ if (browser.headlessTimeout !== undefined && typeof browser.headlessTimeout !== 'number') {
27
+ errors.push('browser.headlessTimeout must be a number');
28
+ }
29
+ if (browser.visibleTimeout !== undefined && typeof browser.visibleTimeout !== 'number') {
30
+ errors.push('browser.visibleTimeout must be a number');
31
+ }
32
+ if (browser.waitUntil !== undefined && !VALID_WAIT_UNTIL.includes(browser.waitUntil)) {
33
+ errors.push(`browser.waitUntil must be one of: ${VALID_WAIT_UNTIL.join(', ')}`);
34
+ }
35
+ }
36
+ // --- storage section ---
37
+ if (!raw.storage || typeof raw.storage !== 'object') {
38
+ errors.push('Missing required section: "storage"');
39
+ }
40
+ else {
41
+ const storage = raw.storage;
42
+ if (typeof storage.credentialsDir !== 'string' || storage.credentialsDir.trim() === '') {
43
+ errors.push('Missing required field: storage.credentialsDir');
44
+ }
45
+ }
46
+ // --- providers section (optional — null/missing means zero providers) ---
47
+ if (raw.providers !== undefined && raw.providers !== null) {
48
+ if (typeof raw.providers !== 'object') {
49
+ errors.push('"providers" must be an object (or omitted)');
50
+ }
51
+ else {
52
+ const providers = raw.providers;
53
+ for (const [id, entry] of Object.entries(providers)) {
54
+ if (!entry || typeof entry !== 'object') {
55
+ errors.push(`Provider "${id}": must be an object`);
56
+ continue;
57
+ }
58
+ const providerErrors = validateProviderEntry(id, entry);
59
+ errors.push(...providerErrors);
60
+ }
61
+ }
62
+ }
63
+ // --- remotes section (optional) ---
64
+ if (raw.remotes !== undefined) {
65
+ if (typeof raw.remotes !== 'object' || raw.remotes === null) {
66
+ errors.push('"remotes" must be an object');
67
+ }
68
+ else {
69
+ const remotes = raw.remotes;
70
+ for (const [name, entry] of Object.entries(remotes)) {
71
+ if (!entry || typeof entry !== 'object') {
72
+ errors.push(`Remote "${name}": must be an object`);
73
+ continue;
74
+ }
75
+ const r = entry;
76
+ if (r.type !== 'ssh') {
77
+ errors.push(`Remote "${name}": only type "ssh" is supported`);
78
+ }
79
+ if (typeof r.host !== 'string' || r.host.trim() === '') {
80
+ errors.push(`Remote "${name}": missing required field "host"`);
81
+ }
82
+ }
83
+ }
84
+ }
85
+ if (errors.length > 0) {
86
+ return err(new ConfigError(`Config validation failed:\n - ${errors.join('\n - ')}`));
87
+ }
88
+ // Build the validated config
89
+ const browserRaw = raw.browser;
90
+ const browser = {
91
+ browserDataDir: browserRaw.browserDataDir,
92
+ channel: browserRaw.channel,
93
+ headlessTimeout: typeof browserRaw.headlessTimeout === 'number' ? browserRaw.headlessTimeout : 30_000,
94
+ visibleTimeout: typeof browserRaw.visibleTimeout === 'number' ? browserRaw.visibleTimeout : 120_000,
95
+ waitUntil: typeof browserRaw.waitUntil === 'string' ? browserRaw.waitUntil : 'load',
96
+ };
97
+ const storageRaw = raw.storage;
98
+ const storage = {
99
+ credentialsDir: storageRaw.credentialsDir,
100
+ };
101
+ const providers = {};
102
+ if (raw.providers && typeof raw.providers === 'object') {
103
+ for (const [id, entry] of Object.entries(raw.providers)) {
104
+ providers[id] = parseProviderEntry(entry);
105
+ }
106
+ }
107
+ let remotes;
108
+ if (raw.remotes && typeof raw.remotes === 'object') {
109
+ remotes = {};
110
+ for (const [name, entry] of Object.entries(raw.remotes)) {
111
+ const r = entry;
112
+ remotes[name] = {
113
+ type: 'ssh',
114
+ host: r.host,
115
+ ...(typeof r.user === 'string' ? { user: r.user } : {}),
116
+ ...(typeof r.path === 'string' ? { path: r.path } : {}),
117
+ ...(typeof r.sshKey === 'string' ? { sshKey: r.sshKey } : {}),
118
+ };
119
+ }
120
+ }
121
+ const config = {
122
+ browser,
123
+ storage,
124
+ providers,
125
+ ...(remotes ? { remotes } : {}),
126
+ };
127
+ return ok(config);
128
+ }
129
+ function validateProviderEntry(id, raw) {
130
+ const errors = [];
131
+ if (!Array.isArray(raw.domains) || raw.domains.length === 0) {
132
+ errors.push(`Provider "${id}": missing required field "domains" (non-empty array)`);
133
+ }
134
+ else {
135
+ for (const d of raw.domains) {
136
+ if (typeof d !== 'string') {
137
+ errors.push(`Provider "${id}": domains must be strings`);
138
+ break;
139
+ }
140
+ }
141
+ }
142
+ if (typeof raw.strategy !== 'string') {
143
+ errors.push(`Provider "${id}": missing required field "strategy"`);
144
+ }
145
+ else if (!VALID_STRATEGIES.includes(raw.strategy)) {
146
+ errors.push(`Provider "${id}": invalid strategy "${raw.strategy}". ` +
147
+ `Valid strategies: ${VALID_STRATEGIES.join(', ')}`);
148
+ }
149
+ // Validate forceVisible at provider level
150
+ if (raw.forceVisible !== undefined && typeof raw.forceVisible !== 'boolean') {
151
+ errors.push(`Provider "${id}": forceVisible must be a boolean`);
152
+ }
153
+ // Validate strategy-specific config shape
154
+ if (typeof raw.strategy === 'string' && raw.config && typeof raw.config === 'object') {
155
+ const strategyErrors = validateStrategyConfig(id, raw.strategy, raw.config);
156
+ errors.push(...strategyErrors);
157
+ }
158
+ return errors;
159
+ }
160
+ function validateStrategyConfig(id, strategy, config) {
161
+ const errors = [];
162
+ // Cross-strategy field checks: warn about fields that don't belong
163
+ if (strategy === 'cookie') {
164
+ const oauthFields = ['audiences', 'tokenEndpoint', 'clientId', 'scopes'];
165
+ for (const field of oauthFields) {
166
+ if (config[field] !== undefined) {
167
+ errors.push(`Provider "${id}": config.${field} is not valid for strategy "cookie"`);
168
+ }
169
+ }
170
+ }
171
+ if (strategy === 'oauth2') {
172
+ const cookieFields = ['ttl', 'requiredCookies'];
173
+ for (const field of cookieFields) {
174
+ if (config[field] !== undefined) {
175
+ errors.push(`Provider "${id}": config.${field} is not valid for strategy "oauth2"`);
176
+ }
177
+ }
178
+ }
179
+ return errors;
180
+ }
181
+ /**
182
+ * Merge a provider entry's strategy + config into a typed StrategyConfig.
183
+ */
184
+ export function buildStrategyConfig(strategy, config) {
185
+ const c = config ?? {};
186
+ switch (strategy) {
187
+ case 'cookie':
188
+ return {
189
+ strategy: 'cookie',
190
+ ...(typeof c.ttl === 'string' ? { ttl: c.ttl } : {}),
191
+ ...(Array.isArray(c.requiredCookies) ? { requiredCookies: c.requiredCookies } : {}),
192
+ };
193
+ case 'oauth2':
194
+ return {
195
+ strategy: 'oauth2',
196
+ ...(Array.isArray(c.audiences) ? { audiences: c.audiences } : {}),
197
+ ...(typeof c.tokenEndpoint === 'string' ? { tokenEndpoint: c.tokenEndpoint } : {}),
198
+ ...(typeof c.clientId === 'string' ? { clientId: c.clientId } : {}),
199
+ ...(Array.isArray(c.scopes) ? { scopes: c.scopes } : {}),
200
+ };
201
+ case 'api-token':
202
+ return {
203
+ strategy: 'api-token',
204
+ ...(typeof c.headerName === 'string' ? { headerName: c.headerName } : {}),
205
+ ...(typeof c.headerPrefix === 'string' ? { headerPrefix: c.headerPrefix } : {}),
206
+ ...(typeof c.setupInstructions === 'string' ? { setupInstructions: c.setupInstructions } : {}),
207
+ };
208
+ case 'basic':
209
+ return {
210
+ strategy: 'basic',
211
+ ...(typeof c.setupInstructions === 'string' ? { setupInstructions: c.setupInstructions } : {}),
212
+ };
213
+ }
214
+ }
215
+ function parseProviderEntry(raw) {
216
+ return {
217
+ ...(typeof raw.name === 'string' ? { name: raw.name } : {}),
218
+ domains: raw.domains,
219
+ ...(typeof raw.entryUrl === 'string' ? { entryUrl: raw.entryUrl } : {}),
220
+ strategy: raw.strategy,
221
+ ...(raw.config && typeof raw.config === 'object' ? { config: raw.config } : {}),
222
+ ...(Array.isArray(raw.acceptedCredentialTypes) ? { acceptedCredentialTypes: raw.acceptedCredentialTypes } : {}),
223
+ ...(typeof raw.setupInstructions === 'string' ? { setupInstructions: raw.setupInstructions } : {}),
224
+ ...(typeof raw.credentialFile === 'string' ? { credentialFile: raw.credentialFile } : {}),
225
+ ...(Array.isArray(raw.xHeaders) ? { xHeaders: raw.xHeaders } : {}),
226
+ ...(typeof raw.forceVisible === 'boolean' ? { forceVisible: raw.forceVisible } : {}),
227
+ };
228
+ }