signet-auth 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of signet-auth might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +393 -0
- package/bin/sig.js +65 -0
- package/dist/auth-manager.d.ts +90 -0
- package/dist/auth-manager.js +262 -0
- package/dist/browser/adapters/playwright.adapter.d.ts +14 -0
- package/dist/browser/adapters/playwright.adapter.js +188 -0
- package/dist/browser/flows/form-login.flow.d.ts +6 -0
- package/dist/browser/flows/form-login.flow.js +35 -0
- package/dist/browser/flows/header-capture.d.ts +23 -0
- package/dist/browser/flows/header-capture.js +104 -0
- package/dist/browser/flows/hybrid-flow.d.ts +37 -0
- package/dist/browser/flows/hybrid-flow.js +104 -0
- package/dist/browser/flows/oauth-consent.flow.d.ts +20 -0
- package/dist/browser/flows/oauth-consent.flow.js +170 -0
- package/dist/cli/commands/doctor.d.ts +6 -0
- package/dist/cli/commands/doctor.js +263 -0
- package/dist/cli/commands/get.d.ts +2 -0
- package/dist/cli/commands/get.js +83 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.js +244 -0
- package/dist/cli/commands/login.d.ts +2 -0
- package/dist/cli/commands/login.js +77 -0
- package/dist/cli/commands/logout.d.ts +2 -0
- package/dist/cli/commands/logout.js +11 -0
- package/dist/cli/commands/providers.d.ts +2 -0
- package/dist/cli/commands/providers.js +30 -0
- package/dist/cli/commands/remote.d.ts +1 -0
- package/dist/cli/commands/remote.js +67 -0
- package/dist/cli/commands/request.d.ts +2 -0
- package/dist/cli/commands/request.js +82 -0
- package/dist/cli/commands/status.d.ts +2 -0
- package/dist/cli/commands/status.js +41 -0
- package/dist/cli/commands/sync.d.ts +2 -0
- package/dist/cli/commands/sync.js +62 -0
- package/dist/cli/formatters.d.ts +3 -0
- package/dist/cli/formatters.js +25 -0
- package/dist/cli/main.d.ts +8 -0
- package/dist/cli/main.js +125 -0
- package/dist/config/generator.d.ts +24 -0
- package/dist/config/generator.js +97 -0
- package/dist/config/loader.d.ts +21 -0
- package/dist/config/loader.js +54 -0
- package/dist/config/schema.d.ts +44 -0
- package/dist/config/schema.js +8 -0
- package/dist/config/validator.d.ts +15 -0
- package/dist/config/validator.js +228 -0
- package/dist/core/errors.d.ts +57 -0
- package/dist/core/errors.js +107 -0
- package/dist/core/interfaces/auth-strategy.d.ts +48 -0
- package/dist/core/interfaces/auth-strategy.js +1 -0
- package/dist/core/interfaces/browser-adapter.d.ts +73 -0
- package/dist/core/interfaces/browser-adapter.js +1 -0
- package/dist/core/interfaces/provider.d.ts +15 -0
- package/dist/core/interfaces/provider.js +1 -0
- package/dist/core/interfaces/storage.d.ts +21 -0
- package/dist/core/interfaces/storage.js +1 -0
- package/dist/core/result.d.ts +21 -0
- package/dist/core/result.js +16 -0
- package/dist/core/types.d.ts +128 -0
- package/dist/core/types.js +6 -0
- package/dist/deps.d.ts +20 -0
- package/dist/deps.js +54 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +37 -0
- package/dist/providers/auto-provision.d.ts +9 -0
- package/dist/providers/auto-provision.js +27 -0
- package/dist/providers/config-loader.d.ts +7 -0
- package/dist/providers/config-loader.js +7 -0
- package/dist/providers/provider-registry.d.ts +19 -0
- package/dist/providers/provider-registry.js +68 -0
- package/dist/storage/cached-storage.d.ts +24 -0
- package/dist/storage/cached-storage.js +57 -0
- package/dist/storage/directory-storage.d.ts +25 -0
- package/dist/storage/directory-storage.js +184 -0
- package/dist/storage/memory-storage.d.ts +14 -0
- package/dist/storage/memory-storage.js +27 -0
- package/dist/strategies/api-token.strategy.d.ts +6 -0
- package/dist/strategies/api-token.strategy.js +63 -0
- package/dist/strategies/basic-auth.strategy.d.ts +6 -0
- package/dist/strategies/basic-auth.strategy.js +41 -0
- package/dist/strategies/cookie.strategy.d.ts +6 -0
- package/dist/strategies/cookie.strategy.js +118 -0
- package/dist/strategies/oauth2.strategy.d.ts +6 -0
- package/dist/strategies/oauth2.strategy.js +134 -0
- package/dist/strategies/registry.d.ts +13 -0
- package/dist/strategies/registry.js +25 -0
- package/dist/sync/remote-config.d.ts +8 -0
- package/dist/sync/remote-config.js +49 -0
- package/dist/sync/sync-engine.d.ts +10 -0
- package/dist/sync/sync-engine.js +96 -0
- package/dist/sync/transports/ssh.d.ts +18 -0
- package/dist/sync/transports/ssh.js +115 -0
- package/dist/sync/types.d.ts +17 -0
- package/dist/sync/types.js +1 -0
- package/dist/utils/duration.d.ts +9 -0
- package/dist/utils/duration.js +34 -0
- package/dist/utils/http.d.ts +4 -0
- package/dist/utils/http.js +10 -0
- package/dist/utils/jwt.d.ts +15 -0
- package/dist/utils/jwt.js +30 -0
- package/package.json +56 -0
- package/src/auth-manager.ts +331 -0
- package/src/browser/adapters/playwright.adapter.ts +247 -0
- package/src/browser/flows/form-login.flow.ts +35 -0
- package/src/browser/flows/header-capture.ts +128 -0
- package/src/browser/flows/hybrid-flow.ts +165 -0
- package/src/browser/flows/oauth-consent.flow.ts +200 -0
- package/src/cli/commands/doctor.ts +301 -0
- package/src/cli/commands/get.ts +96 -0
- package/src/cli/commands/init.ts +289 -0
- package/src/cli/commands/login.ts +94 -0
- package/src/cli/commands/logout.ts +17 -0
- package/src/cli/commands/providers.ts +39 -0
- package/src/cli/commands/remote.ts +71 -0
- package/src/cli/commands/request.ts +97 -0
- package/src/cli/commands/status.ts +48 -0
- package/src/cli/commands/sync.ts +71 -0
- package/src/cli/formatters.ts +31 -0
- package/src/cli/main.ts +144 -0
- package/src/config/generator.ts +122 -0
- package/src/config/loader.ts +70 -0
- package/src/config/schema.ts +75 -0
- package/src/config/validator.ts +281 -0
- package/src/core/errors.ts +182 -0
- package/src/core/interfaces/auth-strategy.ts +65 -0
- package/src/core/interfaces/browser-adapter.ts +81 -0
- package/src/core/interfaces/provider.ts +19 -0
- package/src/core/interfaces/storage.ts +26 -0
- package/src/core/result.ts +24 -0
- package/src/core/types.ts +194 -0
- package/src/deps.ts +80 -0
- package/src/index.ts +109 -0
- package/src/providers/auto-provision.ts +30 -0
- package/src/providers/config-loader.ts +8 -0
- package/src/providers/provider-registry.ts +79 -0
- package/src/storage/cached-storage.ts +72 -0
- package/src/storage/directory-storage.ts +204 -0
- package/src/storage/memory-storage.ts +35 -0
- package/src/strategies/api-token.strategy.ts +87 -0
- package/src/strategies/basic-auth.strategy.ts +64 -0
- package/src/strategies/cookie.strategy.ts +153 -0
- package/src/strategies/oauth2.strategy.ts +178 -0
- package/src/strategies/registry.ts +34 -0
- package/src/sync/remote-config.ts +60 -0
- package/src/sync/sync-engine.ts +113 -0
- package/src/sync/transports/ssh.ts +130 -0
- package/src/sync/types.ts +15 -0
- package/src/utils/duration.ts +34 -0
- package/src/utils/http.ts +11 -0
- package/src/utils/jwt.ts +39 -0
- package/tsconfig.json +20 -0
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,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
|
+
}
|