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
package/src/index.ts ADDED
@@ -0,0 +1,109 @@
1
+ // Public API exports for signet
2
+
3
+ // Config types and loader
4
+ export type {
5
+ SignetConfig,
6
+ BrowserConfig,
7
+ StorageConfig,
8
+ ProviderEntry,
9
+ RemoteEntry,
10
+ } from './config/schema.js';
11
+ export { loadConfig, saveConfig, getConfigPath } from './config/loader.js';
12
+ export { validateConfig, buildStrategyConfig } from './config/validator.js';
13
+ export { generateConfigYaml } from './config/generator.js';
14
+ export type { InitOptions } from './config/generator.js';
15
+
16
+ // Dependency wiring
17
+ export { createAuthDeps } from './deps.js';
18
+ export type { AuthDeps } from './deps.js';
19
+
20
+ // Core types
21
+ export type {
22
+ Credential,
23
+ CookieCredential,
24
+ BearerCredential,
25
+ ApiKeyCredential,
26
+ BasicCredential,
27
+ CredentialType,
28
+ Cookie,
29
+ ProviderConfig,
30
+ StrategyConfig,
31
+ StrategyName,
32
+ CookieStrategyConfig,
33
+ OAuth2StrategyConfig,
34
+ ApiTokenStrategyConfig,
35
+ BasicStrategyConfig,
36
+ StoredCredential,
37
+ StoredEntry,
38
+ ProviderStatus,
39
+ BrowserLaunchOptions,
40
+ ILogger,
41
+ XHeaderConfig,
42
+ AuthDiagnostics,
43
+ } from './core/types.js';
44
+
45
+ // Result type
46
+ export { ok, err, isOk, isErr } from './core/result.js';
47
+ export type { Result } from './core/result.js';
48
+
49
+ // Errors
50
+ export {
51
+ AuthError,
52
+ ProviderNotFoundError,
53
+ CredentialNotFoundError,
54
+ CredentialExpiredError,
55
+ CredentialTypeError,
56
+ RefreshError,
57
+ BrowserError,
58
+ BrowserLaunchError,
59
+ BrowserTimeoutError,
60
+ BrowserNavigationError,
61
+ StorageError,
62
+ ConfigError,
63
+ ManualSetupRequired,
64
+ SyncError,
65
+ RemoteNotFoundError,
66
+ SyncConflictError,
67
+ } from './core/errors.js';
68
+
69
+ // Interfaces (for implementing custom adapters/strategies)
70
+ export type { IAuthStrategy, IAuthStrategyFactory, AuthContext } from './core/interfaces/auth-strategy.js';
71
+ export type { IBrowserAdapter, IBrowserSession, IBrowserPage, NavigateOptions, PageRequest, PageResponse } from './core/interfaces/browser-adapter.js';
72
+ export type { IStorage } from './core/interfaces/storage.js';
73
+ export type { IProviderRegistry } from './core/interfaces/provider.js';
74
+
75
+ // AuthManager
76
+ export { AuthManager } from './auth-manager.js';
77
+
78
+ // Strategy factories (for custom registration)
79
+ export { CookieStrategyFactory } from './strategies/cookie.strategy.js';
80
+ export { OAuth2StrategyFactory } from './strategies/oauth2.strategy.js';
81
+ export { ApiTokenStrategyFactory } from './strategies/api-token.strategy.js';
82
+ export { BasicAuthStrategyFactory } from './strategies/basic-auth.strategy.js';
83
+ export { StrategyRegistry } from './strategies/registry.js';
84
+
85
+ // Storage implementations
86
+ export { DirectoryStorage } from './storage/directory-storage.js';
87
+ export { CachedStorage } from './storage/cached-storage.js';
88
+ export { MemoryStorage } from './storage/memory-storage.js';
89
+
90
+ // Provider system
91
+ export { ProviderRegistry } from './providers/provider-registry.js';
92
+ export { createDefaultProvider } from './providers/auto-provision.js';
93
+
94
+ // Browser adapters
95
+ export { PlaywrightAdapter } from './browser/adapters/playwright.adapter.js';
96
+
97
+ // CLI
98
+ export { run as runCli, parseArgs } from './cli/main.js';
99
+
100
+ // Sync
101
+ export { SyncEngine } from './sync/sync-engine.js';
102
+ export { SshTransport } from './sync/transports/ssh.js';
103
+ export { getRemotes, getRemote, addRemote, removeRemote } from './sync/remote-config.js';
104
+ export type { RemoteConfig, SyncResult } from './sync/types.js';
105
+
106
+ // Utilities
107
+ export { decodeJwt, isJwtExpired, getJwtExpiresAt } from './utils/jwt.js';
108
+ export { parseDuration, formatDuration } from './utils/duration.js';
109
+ export { buildUserAgent } from './utils/http.js';
@@ -0,0 +1,30 @@
1
+ import type { ProviderConfig } from '../core/types.js';
2
+
3
+ /**
4
+ * Create a default provider config from a URL.
5
+ * Used for auto-provisioning when no configured provider matches.
6
+ *
7
+ * Defaults to cookie strategy (most common for SSO).
8
+ * Provider ID = hostname, making it deterministic across restarts.
9
+ */
10
+ export function createDefaultProvider(url: string): ProviderConfig {
11
+ let parsed: URL;
12
+ try {
13
+ parsed = new URL(url);
14
+ } catch {
15
+ // Treat as hostname — try adding https://
16
+ parsed = new URL(`https://${url}`);
17
+ }
18
+
19
+ const hostname = parsed.hostname;
20
+
21
+ return {
22
+ id: hostname,
23
+ name: hostname,
24
+ domains: [hostname],
25
+ entryUrl: `${parsed.protocol}//${parsed.host}/`,
26
+ strategy: 'cookie',
27
+ strategyConfig: { strategy: 'cookie' },
28
+ autoProvisioned: true,
29
+ };
30
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Provider config loader — delegates to the unified config system.
3
+ *
4
+ * This module is kept for backward compatibility. New code should use
5
+ * src/config/loader.ts directly.
6
+ */
7
+
8
+ export { loadConfig } from '../config/loader.js';
@@ -0,0 +1,79 @@
1
+ import type { IProviderRegistry } from '../core/interfaces/provider.js';
2
+ import type { ProviderConfig } from '../core/types.js';
3
+
4
+ /**
5
+ * Registry for provider configurations.
6
+ * Resolves URLs to providers using glob-style domain matching.
7
+ *
8
+ * Domain matching rules:
9
+ * - Exact: "api.example.com" matches only "api.example.com"
10
+ * - Wildcard: "*.example.com" matches "api.example.com", "www.example.com", etc.
11
+ * - Exact matches take priority over wildcard matches.
12
+ */
13
+ export class ProviderRegistry implements IProviderRegistry {
14
+ private providers = new Map<string, ProviderConfig>();
15
+
16
+ constructor(initialProviders: ProviderConfig[] = []) {
17
+ for (const provider of initialProviders) {
18
+ this.providers.set(provider.id, provider);
19
+ }
20
+ }
21
+
22
+ resolve(url: string): ProviderConfig | null {
23
+ let hostname: string;
24
+ try {
25
+ hostname = new URL(url).hostname;
26
+ } catch {
27
+ // If URL parsing fails, treat the input as a hostname
28
+ hostname = url;
29
+ }
30
+
31
+ // First pass: exact domain match (higher priority)
32
+ for (const provider of this.providers.values()) {
33
+ for (const domain of provider.domains) {
34
+ if (!domain.includes('*') && hostname === domain) {
35
+ return provider;
36
+ }
37
+ }
38
+ }
39
+
40
+ // Second pass: glob/wildcard match
41
+ for (const provider of this.providers.values()) {
42
+ for (const domain of provider.domains) {
43
+ if (domain.includes('*') && matchGlob(hostname, domain)) {
44
+ return provider;
45
+ }
46
+ }
47
+ }
48
+
49
+ return null;
50
+ }
51
+
52
+ get(id: string): ProviderConfig | null {
53
+ return this.providers.get(id) ?? null;
54
+ }
55
+
56
+ list(): ProviderConfig[] {
57
+ return Array.from(this.providers.values());
58
+ }
59
+
60
+ register(provider: ProviderConfig): void {
61
+ this.providers.set(provider.id, provider);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Simple glob matching for domain patterns.
67
+ * Supports only "*" as a wildcard segment prefix.
68
+ * Examples:
69
+ * "*.example.com" matches "api.example.com", "www.example.com"
70
+ * "*.*.example.com" matches "a.b.example.com"
71
+ */
72
+ function matchGlob(hostname: string, pattern: string): boolean {
73
+ // Convert glob to regex: *.example.com → ^[^.]+\.example\.com$
74
+ const escaped = pattern
75
+ .replace(/\./g, '\\.')
76
+ .replace(/\*/g, '[^.]+');
77
+ const regex = new RegExp(`^${escaped}$`, 'i');
78
+ return regex.test(hostname);
79
+ }
@@ -0,0 +1,72 @@
1
+ import type { IStorage } from '../core/interfaces/storage.js';
2
+ import type { StoredCredential, StoredEntry } from '../core/types.js';
3
+
4
+ interface CacheEntry {
5
+ value: StoredCredential | null;
6
+ expiresAt: number;
7
+ }
8
+
9
+ /**
10
+ * Decorator that adds a TTL cache over any IStorage implementation.
11
+ * Reads are cached; writes invalidate the cache for the affected key.
12
+ *
13
+ * Usage:
14
+ * const storage = new CachedStorage(new DirectoryStorage(dir), { ttlMs: 5000 });
15
+ */
16
+ export class CachedStorage implements IStorage {
17
+ private cache = new Map<string, CacheEntry>();
18
+ private listCache: { entries: StoredEntry[]; expiresAt: number } | null = null;
19
+
20
+ constructor(
21
+ private readonly inner: IStorage,
22
+ private readonly options: { ttlMs: number } = { ttlMs: 5000 },
23
+ ) {}
24
+
25
+ async get(providerId: string): Promise<StoredCredential | null> {
26
+ const cached = this.cache.get(providerId);
27
+ if (cached && Date.now() < cached.expiresAt) {
28
+ return cached.value;
29
+ }
30
+
31
+ const value = await this.inner.get(providerId);
32
+ this.cache.set(providerId, {
33
+ value,
34
+ expiresAt: Date.now() + this.options.ttlMs,
35
+ });
36
+ return value;
37
+ }
38
+
39
+ async set(providerId: string, credential: StoredCredential): Promise<void> {
40
+ this.invalidate(providerId);
41
+ await this.inner.set(providerId, credential);
42
+ }
43
+
44
+ async delete(providerId: string): Promise<void> {
45
+ this.invalidate(providerId);
46
+ await this.inner.delete(providerId);
47
+ }
48
+
49
+ async list(): Promise<StoredEntry[]> {
50
+ if (this.listCache && Date.now() < this.listCache.expiresAt) {
51
+ return this.listCache.entries;
52
+ }
53
+
54
+ const entries = await this.inner.list();
55
+ this.listCache = {
56
+ entries,
57
+ expiresAt: Date.now() + this.options.ttlMs,
58
+ };
59
+ return entries;
60
+ }
61
+
62
+ async clear(): Promise<void> {
63
+ this.cache.clear();
64
+ this.listCache = null;
65
+ await this.inner.clear();
66
+ }
67
+
68
+ private invalidate(providerId: string): void {
69
+ this.cache.delete(providerId);
70
+ this.listCache = null;
71
+ }
72
+ }
@@ -0,0 +1,204 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import lockfile from 'proper-lockfile';
4
+ import type { IStorage } from '../core/interfaces/storage.js';
5
+ import type { StoredCredential, StoredEntry } from '../core/types.js';
6
+ import { StorageError } from '../core/errors.js';
7
+
8
+ interface ProviderFile {
9
+ version: 1;
10
+ providerId: string;
11
+ credential: StoredCredential['credential'];
12
+ strategy: string;
13
+ updatedAt: string;
14
+ metadata?: Record<string, unknown>;
15
+ }
16
+
17
+ /**
18
+ * Convert a provider ID into a human-readable, filesystem-safe filename.
19
+ * Replaces unsafe characters with underscores.
20
+ */
21
+ function sanitizeId(providerId: string): string {
22
+ return providerId.replace(/[^a-zA-Z0-9._-]/g, '_');
23
+ }
24
+
25
+ /**
26
+ * Per-provider directory-based storage.
27
+ * Each provider's credentials are stored in a separate JSON file
28
+ * under the configured directory: `{dirPath}/{sanitizedProviderId}.json`.
29
+ *
30
+ * Uses per-file advisory locking via `proper-lockfile` and atomic writes
31
+ * (write to tmp + rename) for safe concurrent access.
32
+ */
33
+ export class DirectoryStorage implements IStorage {
34
+ constructor(private readonly dirPath: string) {}
35
+
36
+ async get(providerId: string): Promise<StoredCredential | null> {
37
+ const filePath = this.filePathFor(providerId);
38
+ try {
39
+ const data = await this.readFile(filePath);
40
+ return this.toStoredCredential(data);
41
+ } catch (e: unknown) {
42
+ if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
43
+ return null;
44
+ }
45
+ throw new StorageError('read', (e as Error).message);
46
+ }
47
+ }
48
+
49
+ async set(providerId: string, credential: StoredCredential): Promise<void> {
50
+ const filePath = this.filePathFor(providerId);
51
+ await this.ensureDir();
52
+
53
+ const data: ProviderFile = {
54
+ version: 1,
55
+ providerId,
56
+ credential: credential.credential,
57
+ strategy: credential.strategy,
58
+ updatedAt: credential.updatedAt,
59
+ ...(credential.metadata ? { metadata: credential.metadata } : {}),
60
+ };
61
+
62
+ await this.withLock(filePath, async () => {
63
+ await this.atomicWrite(filePath, data);
64
+ });
65
+ }
66
+
67
+ async delete(providerId: string): Promise<void> {
68
+ const filePath = this.filePathFor(providerId);
69
+ try {
70
+ await fs.unlink(filePath);
71
+ } catch (e: unknown) {
72
+ if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
73
+ return; // No-op if not found
74
+ }
75
+ throw new StorageError('delete', (e as Error).message);
76
+ }
77
+ }
78
+
79
+ async list(): Promise<StoredEntry[]> {
80
+ let files: string[];
81
+ try {
82
+ files = await fs.readdir(this.dirPath);
83
+ } catch (e: unknown) {
84
+ if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
85
+ return [];
86
+ }
87
+ throw new StorageError('list', (e as Error).message);
88
+ }
89
+
90
+ const entries: StoredEntry[] = [];
91
+ for (const file of files) {
92
+ if (!file.endsWith('.json')) continue;
93
+
94
+ const filePath = path.join(this.dirPath, file);
95
+ try {
96
+ const data = await this.readFile(filePath);
97
+ entries.push({
98
+ providerId: data.providerId,
99
+ strategy: data.strategy,
100
+ updatedAt: data.updatedAt,
101
+ credentialType: data.credential.type,
102
+ });
103
+ } catch {
104
+ // Skip files that can't be read or parsed
105
+ continue;
106
+ }
107
+ }
108
+
109
+ return entries;
110
+ }
111
+
112
+ async clear(): Promise<void> {
113
+ let files: string[];
114
+ try {
115
+ files = await fs.readdir(this.dirPath);
116
+ } catch (e: unknown) {
117
+ if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
118
+ return;
119
+ }
120
+ throw new StorageError('clear', (e as Error).message);
121
+ }
122
+
123
+ for (const file of files) {
124
+ if (!file.endsWith('.json')) continue;
125
+
126
+ const filePath = path.join(this.dirPath, file);
127
+ try {
128
+ await fs.unlink(filePath);
129
+ } catch (e: unknown) {
130
+ if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
131
+ throw new StorageError('clear', (e as Error).message);
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Private helpers
139
+ // ---------------------------------------------------------------------------
140
+
141
+ private filePathFor(providerId: string): string {
142
+ return path.join(this.dirPath, `${sanitizeId(providerId)}.json`);
143
+ }
144
+
145
+ private async ensureDir(): Promise<void> {
146
+ await fs.mkdir(this.dirPath, { recursive: true });
147
+ }
148
+
149
+ private async readFile(filePath: string): Promise<ProviderFile> {
150
+ const content = await fs.readFile(filePath, 'utf-8');
151
+ const data = JSON.parse(content) as ProviderFile;
152
+ if (!data.version || !data.providerId || !data.credential) {
153
+ throw new StorageError('read', `Invalid provider file: ${filePath}`);
154
+ }
155
+ return data;
156
+ }
157
+
158
+ private toStoredCredential(data: ProviderFile): StoredCredential {
159
+ return {
160
+ credential: data.credential,
161
+ providerId: data.providerId,
162
+ strategy: data.strategy,
163
+ updatedAt: data.updatedAt,
164
+ ...(data.metadata ? { metadata: data.metadata } : {}),
165
+ };
166
+ }
167
+
168
+ private async atomicWrite(filePath: string, data: ProviderFile): Promise<void> {
169
+ try {
170
+ const content = JSON.stringify(data, null, 2);
171
+ const tmpPath = `${filePath}.tmp.${process.pid}`;
172
+ await fs.writeFile(tmpPath, content, 'utf-8');
173
+ await fs.rename(tmpPath, filePath);
174
+ } catch (e: unknown) {
175
+ throw new StorageError('write', (e as Error).message);
176
+ }
177
+ }
178
+
179
+ private async withLock<T>(filePath: string, fn: () => Promise<T>): Promise<T> {
180
+ await this.ensureDir();
181
+
182
+ // Use a separate .lock file so we never create dummy credential files
183
+ const lockPath = `${filePath}.lock`;
184
+ await fs.writeFile(lockPath, '', { flag: 'a' });
185
+
186
+ let release: (() => Promise<void>) | undefined;
187
+ try {
188
+ release = await lockfile.lock(lockPath, {
189
+ retries: { retries: 5, minTimeout: 100, maxTimeout: 1000 },
190
+ stale: 10000,
191
+ });
192
+ return await fn();
193
+ } catch (e: unknown) {
194
+ if ((e as Error).message?.includes('ELOCKED')) {
195
+ throw new StorageError('lock', 'Could not acquire file lock. Another process may be writing.');
196
+ }
197
+ throw e;
198
+ } finally {
199
+ if (release) {
200
+ await release().catch(() => {});
201
+ }
202
+ }
203
+ }
204
+ }
@@ -0,0 +1,35 @@
1
+ import type { IStorage } from '../core/interfaces/storage.js';
2
+ import type { StoredCredential, StoredEntry } from '../core/types.js';
3
+
4
+ /**
5
+ * In-memory storage implementation for testing.
6
+ * No persistence — data is lost when the process exits.
7
+ */
8
+ export class MemoryStorage implements IStorage {
9
+ private store = new Map<string, StoredCredential>();
10
+
11
+ async get(providerId: string): Promise<StoredCredential | null> {
12
+ return this.store.get(providerId) ?? null;
13
+ }
14
+
15
+ async set(providerId: string, credential: StoredCredential): Promise<void> {
16
+ this.store.set(providerId, credential);
17
+ }
18
+
19
+ async delete(providerId: string): Promise<void> {
20
+ this.store.delete(providerId);
21
+ }
22
+
23
+ async list(): Promise<StoredEntry[]> {
24
+ return Array.from(this.store.entries()).map(([providerId, stored]) => ({
25
+ providerId,
26
+ strategy: stored.strategy,
27
+ updatedAt: stored.updatedAt,
28
+ credentialType: stored.credential.type,
29
+ }));
30
+ }
31
+
32
+ async clear(): Promise<void> {
33
+ this.store.clear();
34
+ }
35
+ }
@@ -0,0 +1,87 @@
1
+ import type { IAuthStrategy, IAuthStrategyFactory, AuthContext } from '../core/interfaces/auth-strategy.js';
2
+ import type { Credential, ApiKeyCredential, ProviderConfig } from '../core/types.js';
3
+ import type { StrategyConfig, ApiTokenStrategyConfig } from '../config/schema.js';
4
+ import type { Result } from '../core/result.js';
5
+ import { ok, err } from '../core/result.js';
6
+ import { ManualSetupRequired, type AuthError } from '../core/errors.js';
7
+ import { decodeJwt } from '../utils/jwt.js';
8
+
9
+ const DEFAULT_HEADER_NAME = 'Authorization';
10
+ const DEFAULT_HEADER_PREFIX = 'Bearer';
11
+ const DEFAULT_SETUP_INSTRUCTIONS = 'Please provide an API token or Personal Access Token.';
12
+
13
+ /**
14
+ * Static API token strategy.
15
+ * User provides the token manually — no browser automation needed.
16
+ * Optionally checks JWT expiry if the token is a JWT.
17
+ */
18
+ class ApiTokenStrategy implements IAuthStrategy {
19
+ private readonly headerName: string;
20
+ private readonly headerPrefix: string;
21
+ private readonly setupInstructions: string;
22
+
23
+ constructor(config: ApiTokenStrategyConfig) {
24
+ this.headerName = config.headerName ?? DEFAULT_HEADER_NAME;
25
+ this.headerPrefix = config.headerPrefix ?? DEFAULT_HEADER_PREFIX;
26
+ this.setupInstructions = config.setupInstructions ?? DEFAULT_SETUP_INSTRUCTIONS;
27
+ }
28
+
29
+ validate(credential: Credential): Result<boolean, AuthError> {
30
+ if (credential.type !== 'api-key') {
31
+ return ok(false);
32
+ }
33
+
34
+ if (!credential.key || credential.key.trim() === '') {
35
+ return ok(false);
36
+ }
37
+
38
+ // If the key looks like a JWT, check its expiry
39
+ const jwt = decodeJwt(credential.key);
40
+ if (jwt?.exp) {
41
+ const expiresAtMs = jwt.exp * 1000;
42
+ if (Date.now() >= expiresAtMs) {
43
+ return ok(false);
44
+ }
45
+ }
46
+
47
+ return ok(true);
48
+ }
49
+
50
+ async authenticate(
51
+ provider: ProviderConfig,
52
+ ): Promise<Result<Credential, AuthError>> {
53
+ // API tokens cannot be obtained automatically — user must provide them.
54
+ return err(
55
+ new ManualSetupRequired(
56
+ provider.id,
57
+ this.setupInstructions,
58
+ ),
59
+ );
60
+ }
61
+
62
+ async refresh(): Promise<Result<Credential | null, AuthError>> {
63
+ // Static tokens cannot be refreshed
64
+ return ok(null);
65
+ }
66
+
67
+ applyToRequest(credential: Credential): Record<string, string> {
68
+ if (credential.type !== 'api-key') return {};
69
+
70
+ const value = this.headerPrefix
71
+ ? `${this.headerPrefix} ${credential.key}`
72
+ : credential.key;
73
+
74
+ return { [this.headerName]: value };
75
+ }
76
+ }
77
+
78
+ export class ApiTokenStrategyFactory implements IAuthStrategyFactory {
79
+ readonly name = 'api-token';
80
+
81
+ create(config: StrategyConfig): IAuthStrategy {
82
+ if (config.strategy !== 'api-token') {
83
+ throw new Error(`ApiTokenStrategyFactory received wrong config type: ${config.strategy}`);
84
+ }
85
+ return new ApiTokenStrategy(config);
86
+ }
87
+ }
@@ -0,0 +1,64 @@
1
+ import type { IAuthStrategy, IAuthStrategyFactory } from '../core/interfaces/auth-strategy.js';
2
+ import type { Credential, ProviderConfig } from '../core/types.js';
3
+ import type { StrategyConfig, BasicStrategyConfig } from '../config/schema.js';
4
+ import type { Result } from '../core/result.js';
5
+ import { ok, err } from '../core/result.js';
6
+ import { ManualSetupRequired, type AuthError } from '../core/errors.js';
7
+
8
+ /**
9
+ * Basic authentication strategy.
10
+ * User provides username + password — no browser automation needed.
11
+ * Produces an Authorization: Basic <base64> header.
12
+ */
13
+ class BasicAuthStrategy implements IAuthStrategy {
14
+ private readonly setupInstructions?: string;
15
+
16
+ constructor(config: BasicStrategyConfig) {
17
+ this.setupInstructions = config.setupInstructions;
18
+ }
19
+
20
+ validate(credential: Credential): Result<boolean, AuthError> {
21
+ if (credential.type !== 'basic') return ok(false);
22
+ return ok(
23
+ credential.username.length > 0 && credential.password.length > 0,
24
+ );
25
+ }
26
+
27
+ async authenticate(
28
+ provider: ProviderConfig,
29
+ ): Promise<Result<Credential, AuthError>> {
30
+ return err(
31
+ new ManualSetupRequired(
32
+ provider.id,
33
+ this.setupInstructions ??
34
+ provider.setupInstructions ??
35
+ 'Please provide username and password for basic authentication.',
36
+ ),
37
+ );
38
+ }
39
+
40
+ async refresh(): Promise<Result<Credential | null, AuthError>> {
41
+ return ok(null);
42
+ }
43
+
44
+ applyToRequest(credential: Credential): Record<string, string> {
45
+ if (credential.type !== 'basic') return {};
46
+
47
+ const encoded = Buffer.from(
48
+ `${credential.username}:${credential.password}`,
49
+ ).toString('base64');
50
+
51
+ return { Authorization: `Basic ${encoded}` };
52
+ }
53
+ }
54
+
55
+ export class BasicAuthStrategyFactory implements IAuthStrategyFactory {
56
+ readonly name = 'basic';
57
+
58
+ create(config: StrategyConfig): IAuthStrategy {
59
+ if (config.strategy !== 'basic') {
60
+ throw new Error(`BasicAuthStrategyFactory received wrong config type: ${config.strategy}`);
61
+ }
62
+ return new BasicAuthStrategy(config);
63
+ }
64
+ }