signet-auth 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of signet-auth might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +393 -0
- package/bin/sig.js +65 -0
- package/dist/auth-manager.d.ts +90 -0
- package/dist/auth-manager.js +262 -0
- package/dist/browser/adapters/playwright.adapter.d.ts +14 -0
- package/dist/browser/adapters/playwright.adapter.js +188 -0
- package/dist/browser/flows/form-login.flow.d.ts +6 -0
- package/dist/browser/flows/form-login.flow.js +35 -0
- package/dist/browser/flows/header-capture.d.ts +23 -0
- package/dist/browser/flows/header-capture.js +104 -0
- package/dist/browser/flows/hybrid-flow.d.ts +37 -0
- package/dist/browser/flows/hybrid-flow.js +104 -0
- package/dist/browser/flows/oauth-consent.flow.d.ts +20 -0
- package/dist/browser/flows/oauth-consent.flow.js +170 -0
- package/dist/cli/commands/doctor.d.ts +6 -0
- package/dist/cli/commands/doctor.js +263 -0
- package/dist/cli/commands/get.d.ts +2 -0
- package/dist/cli/commands/get.js +83 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.js +244 -0
- package/dist/cli/commands/login.d.ts +2 -0
- package/dist/cli/commands/login.js +77 -0
- package/dist/cli/commands/logout.d.ts +2 -0
- package/dist/cli/commands/logout.js +11 -0
- package/dist/cli/commands/providers.d.ts +2 -0
- package/dist/cli/commands/providers.js +30 -0
- package/dist/cli/commands/remote.d.ts +1 -0
- package/dist/cli/commands/remote.js +67 -0
- package/dist/cli/commands/request.d.ts +2 -0
- package/dist/cli/commands/request.js +82 -0
- package/dist/cli/commands/status.d.ts +2 -0
- package/dist/cli/commands/status.js +41 -0
- package/dist/cli/commands/sync.d.ts +2 -0
- package/dist/cli/commands/sync.js +62 -0
- package/dist/cli/formatters.d.ts +3 -0
- package/dist/cli/formatters.js +25 -0
- package/dist/cli/main.d.ts +8 -0
- package/dist/cli/main.js +125 -0
- package/dist/config/generator.d.ts +24 -0
- package/dist/config/generator.js +97 -0
- package/dist/config/loader.d.ts +21 -0
- package/dist/config/loader.js +54 -0
- package/dist/config/schema.d.ts +44 -0
- package/dist/config/schema.js +8 -0
- package/dist/config/validator.d.ts +15 -0
- package/dist/config/validator.js +228 -0
- package/dist/core/errors.d.ts +57 -0
- package/dist/core/errors.js +107 -0
- package/dist/core/interfaces/auth-strategy.d.ts +48 -0
- package/dist/core/interfaces/auth-strategy.js +1 -0
- package/dist/core/interfaces/browser-adapter.d.ts +73 -0
- package/dist/core/interfaces/browser-adapter.js +1 -0
- package/dist/core/interfaces/provider.d.ts +15 -0
- package/dist/core/interfaces/provider.js +1 -0
- package/dist/core/interfaces/storage.d.ts +21 -0
- package/dist/core/interfaces/storage.js +1 -0
- package/dist/core/result.d.ts +21 -0
- package/dist/core/result.js +16 -0
- package/dist/core/types.d.ts +128 -0
- package/dist/core/types.js +6 -0
- package/dist/deps.d.ts +20 -0
- package/dist/deps.js +54 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +37 -0
- package/dist/providers/auto-provision.d.ts +9 -0
- package/dist/providers/auto-provision.js +27 -0
- package/dist/providers/config-loader.d.ts +7 -0
- package/dist/providers/config-loader.js +7 -0
- package/dist/providers/provider-registry.d.ts +19 -0
- package/dist/providers/provider-registry.js +68 -0
- package/dist/storage/cached-storage.d.ts +24 -0
- package/dist/storage/cached-storage.js +57 -0
- package/dist/storage/directory-storage.d.ts +25 -0
- package/dist/storage/directory-storage.js +184 -0
- package/dist/storage/memory-storage.d.ts +14 -0
- package/dist/storage/memory-storage.js +27 -0
- package/dist/strategies/api-token.strategy.d.ts +6 -0
- package/dist/strategies/api-token.strategy.js +63 -0
- package/dist/strategies/basic-auth.strategy.d.ts +6 -0
- package/dist/strategies/basic-auth.strategy.js +41 -0
- package/dist/strategies/cookie.strategy.d.ts +6 -0
- package/dist/strategies/cookie.strategy.js +118 -0
- package/dist/strategies/oauth2.strategy.d.ts +6 -0
- package/dist/strategies/oauth2.strategy.js +134 -0
- package/dist/strategies/registry.d.ts +13 -0
- package/dist/strategies/registry.js +25 -0
- package/dist/sync/remote-config.d.ts +8 -0
- package/dist/sync/remote-config.js +49 -0
- package/dist/sync/sync-engine.d.ts +10 -0
- package/dist/sync/sync-engine.js +96 -0
- package/dist/sync/transports/ssh.d.ts +18 -0
- package/dist/sync/transports/ssh.js +115 -0
- package/dist/sync/types.d.ts +17 -0
- package/dist/sync/types.js +1 -0
- package/dist/utils/duration.d.ts +9 -0
- package/dist/utils/duration.js +34 -0
- package/dist/utils/http.d.ts +4 -0
- package/dist/utils/http.js +10 -0
- package/dist/utils/jwt.d.ts +15 -0
- package/dist/utils/jwt.js +30 -0
- package/package.json +56 -0
- package/src/auth-manager.ts +331 -0
- package/src/browser/adapters/playwright.adapter.ts +247 -0
- package/src/browser/flows/form-login.flow.ts +35 -0
- package/src/browser/flows/header-capture.ts +128 -0
- package/src/browser/flows/hybrid-flow.ts +165 -0
- package/src/browser/flows/oauth-consent.flow.ts +200 -0
- package/src/cli/commands/doctor.ts +301 -0
- package/src/cli/commands/get.ts +96 -0
- package/src/cli/commands/init.ts +289 -0
- package/src/cli/commands/login.ts +94 -0
- package/src/cli/commands/logout.ts +17 -0
- package/src/cli/commands/providers.ts +39 -0
- package/src/cli/commands/remote.ts +71 -0
- package/src/cli/commands/request.ts +97 -0
- package/src/cli/commands/status.ts +48 -0
- package/src/cli/commands/sync.ts +71 -0
- package/src/cli/formatters.ts +31 -0
- package/src/cli/main.ts +144 -0
- package/src/config/generator.ts +122 -0
- package/src/config/loader.ts +70 -0
- package/src/config/schema.ts +75 -0
- package/src/config/validator.ts +281 -0
- package/src/core/errors.ts +182 -0
- package/src/core/interfaces/auth-strategy.ts +65 -0
- package/src/core/interfaces/browser-adapter.ts +81 -0
- package/src/core/interfaces/provider.ts +19 -0
- package/src/core/interfaces/storage.ts +26 -0
- package/src/core/result.ts +24 -0
- package/src/core/types.ts +194 -0
- package/src/deps.ts +80 -0
- package/src/index.ts +109 -0
- package/src/providers/auto-provision.ts +30 -0
- package/src/providers/config-loader.ts +8 -0
- package/src/providers/provider-registry.ts +79 -0
- package/src/storage/cached-storage.ts +72 -0
- package/src/storage/directory-storage.ts +204 -0
- package/src/storage/memory-storage.ts +35 -0
- package/src/strategies/api-token.strategy.ts +87 -0
- package/src/strategies/basic-auth.strategy.ts +64 -0
- package/src/strategies/cookie.strategy.ts +153 -0
- package/src/strategies/oauth2.strategy.ts +178 -0
- package/src/strategies/registry.ts +34 -0
- package/src/sync/remote-config.ts +60 -0
- package/src/sync/sync-engine.ts +113 -0
- package/src/sync/transports/ssh.ts +130 -0
- package/src/sync/types.ts +15 -0
- package/src/utils/duration.ts +34 -0
- package/src/utils/http.ts +11 -0
- package/src/utils/jwt.ts +39 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ProviderConfig } from '../core/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a default provider config from a URL.
|
|
4
|
+
* Used for auto-provisioning when no configured provider matches.
|
|
5
|
+
*
|
|
6
|
+
* Defaults to cookie strategy (most common for SSO).
|
|
7
|
+
* Provider ID = hostname, making it deterministic across restarts.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createDefaultProvider(url: string): ProviderConfig;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a default provider config from a URL.
|
|
3
|
+
* Used for auto-provisioning when no configured provider matches.
|
|
4
|
+
*
|
|
5
|
+
* Defaults to cookie strategy (most common for SSO).
|
|
6
|
+
* Provider ID = hostname, making it deterministic across restarts.
|
|
7
|
+
*/
|
|
8
|
+
export function createDefaultProvider(url) {
|
|
9
|
+
let parsed;
|
|
10
|
+
try {
|
|
11
|
+
parsed = new URL(url);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Treat as hostname — try adding https://
|
|
15
|
+
parsed = new URL(`https://${url}`);
|
|
16
|
+
}
|
|
17
|
+
const hostname = parsed.hostname;
|
|
18
|
+
return {
|
|
19
|
+
id: hostname,
|
|
20
|
+
name: hostname,
|
|
21
|
+
domains: [hostname],
|
|
22
|
+
entryUrl: `${parsed.protocol}//${parsed.host}/`,
|
|
23
|
+
strategy: 'cookie',
|
|
24
|
+
strategyConfig: { strategy: 'cookie' },
|
|
25
|
+
autoProvisioned: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { IProviderRegistry } from '../core/interfaces/provider.js';
|
|
2
|
+
import type { ProviderConfig } from '../core/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Registry for provider configurations.
|
|
5
|
+
* Resolves URLs to providers using glob-style domain matching.
|
|
6
|
+
*
|
|
7
|
+
* Domain matching rules:
|
|
8
|
+
* - Exact: "api.example.com" matches only "api.example.com"
|
|
9
|
+
* - Wildcard: "*.example.com" matches "api.example.com", "www.example.com", etc.
|
|
10
|
+
* - Exact matches take priority over wildcard matches.
|
|
11
|
+
*/
|
|
12
|
+
export declare class ProviderRegistry implements IProviderRegistry {
|
|
13
|
+
private providers;
|
|
14
|
+
constructor(initialProviders?: ProviderConfig[]);
|
|
15
|
+
resolve(url: string): ProviderConfig | null;
|
|
16
|
+
get(id: string): ProviderConfig | null;
|
|
17
|
+
list(): ProviderConfig[];
|
|
18
|
+
register(provider: ProviderConfig): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry for provider configurations.
|
|
3
|
+
* Resolves URLs to providers using glob-style domain matching.
|
|
4
|
+
*
|
|
5
|
+
* Domain matching rules:
|
|
6
|
+
* - Exact: "api.example.com" matches only "api.example.com"
|
|
7
|
+
* - Wildcard: "*.example.com" matches "api.example.com", "www.example.com", etc.
|
|
8
|
+
* - Exact matches take priority over wildcard matches.
|
|
9
|
+
*/
|
|
10
|
+
export class ProviderRegistry {
|
|
11
|
+
providers = new Map();
|
|
12
|
+
constructor(initialProviders = []) {
|
|
13
|
+
for (const provider of initialProviders) {
|
|
14
|
+
this.providers.set(provider.id, provider);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
resolve(url) {
|
|
18
|
+
let hostname;
|
|
19
|
+
try {
|
|
20
|
+
hostname = new URL(url).hostname;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// If URL parsing fails, treat the input as a hostname
|
|
24
|
+
hostname = url;
|
|
25
|
+
}
|
|
26
|
+
// First pass: exact domain match (higher priority)
|
|
27
|
+
for (const provider of this.providers.values()) {
|
|
28
|
+
for (const domain of provider.domains) {
|
|
29
|
+
if (!domain.includes('*') && hostname === domain) {
|
|
30
|
+
return provider;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Second pass: glob/wildcard match
|
|
35
|
+
for (const provider of this.providers.values()) {
|
|
36
|
+
for (const domain of provider.domains) {
|
|
37
|
+
if (domain.includes('*') && matchGlob(hostname, domain)) {
|
|
38
|
+
return provider;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
get(id) {
|
|
45
|
+
return this.providers.get(id) ?? null;
|
|
46
|
+
}
|
|
47
|
+
list() {
|
|
48
|
+
return Array.from(this.providers.values());
|
|
49
|
+
}
|
|
50
|
+
register(provider) {
|
|
51
|
+
this.providers.set(provider.id, provider);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Simple glob matching for domain patterns.
|
|
56
|
+
* Supports only "*" as a wildcard segment prefix.
|
|
57
|
+
* Examples:
|
|
58
|
+
* "*.example.com" matches "api.example.com", "www.example.com"
|
|
59
|
+
* "*.*.example.com" matches "a.b.example.com"
|
|
60
|
+
*/
|
|
61
|
+
function matchGlob(hostname, pattern) {
|
|
62
|
+
// Convert glob to regex: *.example.com → ^[^.]+\.example\.com$
|
|
63
|
+
const escaped = pattern
|
|
64
|
+
.replace(/\./g, '\\.')
|
|
65
|
+
.replace(/\*/g, '[^.]+');
|
|
66
|
+
const regex = new RegExp(`^${escaped}$`, 'i');
|
|
67
|
+
return regex.test(hostname);
|
|
68
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { IStorage } from '../core/interfaces/storage.js';
|
|
2
|
+
import type { StoredCredential, StoredEntry } from '../core/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Decorator that adds a TTL cache over any IStorage implementation.
|
|
5
|
+
* Reads are cached; writes invalidate the cache for the affected key.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const storage = new CachedStorage(new DirectoryStorage(dir), { ttlMs: 5000 });
|
|
9
|
+
*/
|
|
10
|
+
export declare class CachedStorage implements IStorage {
|
|
11
|
+
private readonly inner;
|
|
12
|
+
private readonly options;
|
|
13
|
+
private cache;
|
|
14
|
+
private listCache;
|
|
15
|
+
constructor(inner: IStorage, options?: {
|
|
16
|
+
ttlMs: number;
|
|
17
|
+
});
|
|
18
|
+
get(providerId: string): Promise<StoredCredential | null>;
|
|
19
|
+
set(providerId: string, credential: StoredCredential): Promise<void>;
|
|
20
|
+
delete(providerId: string): Promise<void>;
|
|
21
|
+
list(): Promise<StoredEntry[]>;
|
|
22
|
+
clear(): Promise<void>;
|
|
23
|
+
private invalidate;
|
|
24
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decorator that adds a TTL cache over any IStorage implementation.
|
|
3
|
+
* Reads are cached; writes invalidate the cache for the affected key.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* const storage = new CachedStorage(new DirectoryStorage(dir), { ttlMs: 5000 });
|
|
7
|
+
*/
|
|
8
|
+
export class CachedStorage {
|
|
9
|
+
inner;
|
|
10
|
+
options;
|
|
11
|
+
cache = new Map();
|
|
12
|
+
listCache = null;
|
|
13
|
+
constructor(inner, options = { ttlMs: 5000 }) {
|
|
14
|
+
this.inner = inner;
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
async get(providerId) {
|
|
18
|
+
const cached = this.cache.get(providerId);
|
|
19
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
20
|
+
return cached.value;
|
|
21
|
+
}
|
|
22
|
+
const value = await this.inner.get(providerId);
|
|
23
|
+
this.cache.set(providerId, {
|
|
24
|
+
value,
|
|
25
|
+
expiresAt: Date.now() + this.options.ttlMs,
|
|
26
|
+
});
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
async set(providerId, credential) {
|
|
30
|
+
this.invalidate(providerId);
|
|
31
|
+
await this.inner.set(providerId, credential);
|
|
32
|
+
}
|
|
33
|
+
async delete(providerId) {
|
|
34
|
+
this.invalidate(providerId);
|
|
35
|
+
await this.inner.delete(providerId);
|
|
36
|
+
}
|
|
37
|
+
async list() {
|
|
38
|
+
if (this.listCache && Date.now() < this.listCache.expiresAt) {
|
|
39
|
+
return this.listCache.entries;
|
|
40
|
+
}
|
|
41
|
+
const entries = await this.inner.list();
|
|
42
|
+
this.listCache = {
|
|
43
|
+
entries,
|
|
44
|
+
expiresAt: Date.now() + this.options.ttlMs,
|
|
45
|
+
};
|
|
46
|
+
return entries;
|
|
47
|
+
}
|
|
48
|
+
async clear() {
|
|
49
|
+
this.cache.clear();
|
|
50
|
+
this.listCache = null;
|
|
51
|
+
await this.inner.clear();
|
|
52
|
+
}
|
|
53
|
+
invalidate(providerId) {
|
|
54
|
+
this.cache.delete(providerId);
|
|
55
|
+
this.listCache = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IStorage } from '../core/interfaces/storage.js';
|
|
2
|
+
import type { StoredCredential, StoredEntry } from '../core/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Per-provider directory-based storage.
|
|
5
|
+
* Each provider's credentials are stored in a separate JSON file
|
|
6
|
+
* under the configured directory: `{dirPath}/{sanitizedProviderId}.json`.
|
|
7
|
+
*
|
|
8
|
+
* Uses per-file advisory locking via `proper-lockfile` and atomic writes
|
|
9
|
+
* (write to tmp + rename) for safe concurrent access.
|
|
10
|
+
*/
|
|
11
|
+
export declare class DirectoryStorage implements IStorage {
|
|
12
|
+
private readonly dirPath;
|
|
13
|
+
constructor(dirPath: string);
|
|
14
|
+
get(providerId: string): Promise<StoredCredential | null>;
|
|
15
|
+
set(providerId: string, credential: StoredCredential): Promise<void>;
|
|
16
|
+
delete(providerId: string): Promise<void>;
|
|
17
|
+
list(): Promise<StoredEntry[]>;
|
|
18
|
+
clear(): Promise<void>;
|
|
19
|
+
private filePathFor;
|
|
20
|
+
private ensureDir;
|
|
21
|
+
private readFile;
|
|
22
|
+
private toStoredCredential;
|
|
23
|
+
private atomicWrite;
|
|
24
|
+
private withLock;
|
|
25
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import lockfile from 'proper-lockfile';
|
|
4
|
+
import { StorageError } from '../core/errors.js';
|
|
5
|
+
/**
|
|
6
|
+
* Convert a provider ID into a human-readable, filesystem-safe filename.
|
|
7
|
+
* Replaces unsafe characters with underscores.
|
|
8
|
+
*/
|
|
9
|
+
function sanitizeId(providerId) {
|
|
10
|
+
return providerId.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Per-provider directory-based storage.
|
|
14
|
+
* Each provider's credentials are stored in a separate JSON file
|
|
15
|
+
* under the configured directory: `{dirPath}/{sanitizedProviderId}.json`.
|
|
16
|
+
*
|
|
17
|
+
* Uses per-file advisory locking via `proper-lockfile` and atomic writes
|
|
18
|
+
* (write to tmp + rename) for safe concurrent access.
|
|
19
|
+
*/
|
|
20
|
+
export class DirectoryStorage {
|
|
21
|
+
dirPath;
|
|
22
|
+
constructor(dirPath) {
|
|
23
|
+
this.dirPath = dirPath;
|
|
24
|
+
}
|
|
25
|
+
async get(providerId) {
|
|
26
|
+
const filePath = this.filePathFor(providerId);
|
|
27
|
+
try {
|
|
28
|
+
const data = await this.readFile(filePath);
|
|
29
|
+
return this.toStoredCredential(data);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
if (e.code === 'ENOENT') {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
throw new StorageError('read', e.message);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async set(providerId, credential) {
|
|
39
|
+
const filePath = this.filePathFor(providerId);
|
|
40
|
+
await this.ensureDir();
|
|
41
|
+
const data = {
|
|
42
|
+
version: 1,
|
|
43
|
+
providerId,
|
|
44
|
+
credential: credential.credential,
|
|
45
|
+
strategy: credential.strategy,
|
|
46
|
+
updatedAt: credential.updatedAt,
|
|
47
|
+
...(credential.metadata ? { metadata: credential.metadata } : {}),
|
|
48
|
+
};
|
|
49
|
+
await this.withLock(filePath, async () => {
|
|
50
|
+
await this.atomicWrite(filePath, data);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async delete(providerId) {
|
|
54
|
+
const filePath = this.filePathFor(providerId);
|
|
55
|
+
try {
|
|
56
|
+
await fs.unlink(filePath);
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
if (e.code === 'ENOENT') {
|
|
60
|
+
return; // No-op if not found
|
|
61
|
+
}
|
|
62
|
+
throw new StorageError('delete', e.message);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async list() {
|
|
66
|
+
let files;
|
|
67
|
+
try {
|
|
68
|
+
files = await fs.readdir(this.dirPath);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
if (e.code === 'ENOENT') {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
throw new StorageError('list', e.message);
|
|
75
|
+
}
|
|
76
|
+
const entries = [];
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
if (!file.endsWith('.json'))
|
|
79
|
+
continue;
|
|
80
|
+
const filePath = path.join(this.dirPath, file);
|
|
81
|
+
try {
|
|
82
|
+
const data = await this.readFile(filePath);
|
|
83
|
+
entries.push({
|
|
84
|
+
providerId: data.providerId,
|
|
85
|
+
strategy: data.strategy,
|
|
86
|
+
updatedAt: data.updatedAt,
|
|
87
|
+
credentialType: data.credential.type,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Skip files that can't be read or parsed
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return entries;
|
|
96
|
+
}
|
|
97
|
+
async clear() {
|
|
98
|
+
let files;
|
|
99
|
+
try {
|
|
100
|
+
files = await fs.readdir(this.dirPath);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
if (e.code === 'ENOENT') {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
throw new StorageError('clear', e.message);
|
|
107
|
+
}
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
if (!file.endsWith('.json'))
|
|
110
|
+
continue;
|
|
111
|
+
const filePath = path.join(this.dirPath, file);
|
|
112
|
+
try {
|
|
113
|
+
await fs.unlink(filePath);
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
if (e.code !== 'ENOENT') {
|
|
117
|
+
throw new StorageError('clear', e.message);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Private helpers
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
filePathFor(providerId) {
|
|
126
|
+
return path.join(this.dirPath, `${sanitizeId(providerId)}.json`);
|
|
127
|
+
}
|
|
128
|
+
async ensureDir() {
|
|
129
|
+
await fs.mkdir(this.dirPath, { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
async readFile(filePath) {
|
|
132
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
133
|
+
const data = JSON.parse(content);
|
|
134
|
+
if (!data.version || !data.providerId || !data.credential) {
|
|
135
|
+
throw new StorageError('read', `Invalid provider file: ${filePath}`);
|
|
136
|
+
}
|
|
137
|
+
return data;
|
|
138
|
+
}
|
|
139
|
+
toStoredCredential(data) {
|
|
140
|
+
return {
|
|
141
|
+
credential: data.credential,
|
|
142
|
+
providerId: data.providerId,
|
|
143
|
+
strategy: data.strategy,
|
|
144
|
+
updatedAt: data.updatedAt,
|
|
145
|
+
...(data.metadata ? { metadata: data.metadata } : {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async atomicWrite(filePath, data) {
|
|
149
|
+
try {
|
|
150
|
+
const content = JSON.stringify(data, null, 2);
|
|
151
|
+
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
152
|
+
await fs.writeFile(tmpPath, content, 'utf-8');
|
|
153
|
+
await fs.rename(tmpPath, filePath);
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
throw new StorageError('write', e.message);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async withLock(filePath, fn) {
|
|
160
|
+
await this.ensureDir();
|
|
161
|
+
// Use a separate .lock file so we never create dummy credential files
|
|
162
|
+
const lockPath = `${filePath}.lock`;
|
|
163
|
+
await fs.writeFile(lockPath, '', { flag: 'a' });
|
|
164
|
+
let release;
|
|
165
|
+
try {
|
|
166
|
+
release = await lockfile.lock(lockPath, {
|
|
167
|
+
retries: { retries: 5, minTimeout: 100, maxTimeout: 1000 },
|
|
168
|
+
stale: 10000,
|
|
169
|
+
});
|
|
170
|
+
return await fn();
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
if (e.message?.includes('ELOCKED')) {
|
|
174
|
+
throw new StorageError('lock', 'Could not acquire file lock. Another process may be writing.');
|
|
175
|
+
}
|
|
176
|
+
throw e;
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
if (release) {
|
|
180
|
+
await release().catch(() => { });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IStorage } from '../core/interfaces/storage.js';
|
|
2
|
+
import type { StoredCredential, StoredEntry } from '../core/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* In-memory storage implementation for testing.
|
|
5
|
+
* No persistence — data is lost when the process exits.
|
|
6
|
+
*/
|
|
7
|
+
export declare class MemoryStorage implements IStorage {
|
|
8
|
+
private store;
|
|
9
|
+
get(providerId: string): Promise<StoredCredential | null>;
|
|
10
|
+
set(providerId: string, credential: StoredCredential): Promise<void>;
|
|
11
|
+
delete(providerId: string): Promise<void>;
|
|
12
|
+
list(): Promise<StoredEntry[]>;
|
|
13
|
+
clear(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory storage implementation for testing.
|
|
3
|
+
* No persistence — data is lost when the process exits.
|
|
4
|
+
*/
|
|
5
|
+
export class MemoryStorage {
|
|
6
|
+
store = new Map();
|
|
7
|
+
async get(providerId) {
|
|
8
|
+
return this.store.get(providerId) ?? null;
|
|
9
|
+
}
|
|
10
|
+
async set(providerId, credential) {
|
|
11
|
+
this.store.set(providerId, credential);
|
|
12
|
+
}
|
|
13
|
+
async delete(providerId) {
|
|
14
|
+
this.store.delete(providerId);
|
|
15
|
+
}
|
|
16
|
+
async list() {
|
|
17
|
+
return Array.from(this.store.entries()).map(([providerId, stored]) => ({
|
|
18
|
+
providerId,
|
|
19
|
+
strategy: stored.strategy,
|
|
20
|
+
updatedAt: stored.updatedAt,
|
|
21
|
+
credentialType: stored.credential.type,
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
async clear() {
|
|
25
|
+
this.store.clear();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { IAuthStrategy, IAuthStrategyFactory } from '../core/interfaces/auth-strategy.js';
|
|
2
|
+
import type { StrategyConfig } from '../config/schema.js';
|
|
3
|
+
export declare class ApiTokenStrategyFactory implements IAuthStrategyFactory {
|
|
4
|
+
readonly name = "api-token";
|
|
5
|
+
create(config: StrategyConfig): IAuthStrategy;
|
|
6
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ok, err } from '../core/result.js';
|
|
2
|
+
import { ManualSetupRequired } from '../core/errors.js';
|
|
3
|
+
import { decodeJwt } from '../utils/jwt.js';
|
|
4
|
+
const DEFAULT_HEADER_NAME = 'Authorization';
|
|
5
|
+
const DEFAULT_HEADER_PREFIX = 'Bearer';
|
|
6
|
+
const DEFAULT_SETUP_INSTRUCTIONS = 'Please provide an API token or Personal Access Token.';
|
|
7
|
+
/**
|
|
8
|
+
* Static API token strategy.
|
|
9
|
+
* User provides the token manually — no browser automation needed.
|
|
10
|
+
* Optionally checks JWT expiry if the token is a JWT.
|
|
11
|
+
*/
|
|
12
|
+
class ApiTokenStrategy {
|
|
13
|
+
headerName;
|
|
14
|
+
headerPrefix;
|
|
15
|
+
setupInstructions;
|
|
16
|
+
constructor(config) {
|
|
17
|
+
this.headerName = config.headerName ?? DEFAULT_HEADER_NAME;
|
|
18
|
+
this.headerPrefix = config.headerPrefix ?? DEFAULT_HEADER_PREFIX;
|
|
19
|
+
this.setupInstructions = config.setupInstructions ?? DEFAULT_SETUP_INSTRUCTIONS;
|
|
20
|
+
}
|
|
21
|
+
validate(credential) {
|
|
22
|
+
if (credential.type !== 'api-key') {
|
|
23
|
+
return ok(false);
|
|
24
|
+
}
|
|
25
|
+
if (!credential.key || credential.key.trim() === '') {
|
|
26
|
+
return ok(false);
|
|
27
|
+
}
|
|
28
|
+
// If the key looks like a JWT, check its expiry
|
|
29
|
+
const jwt = decodeJwt(credential.key);
|
|
30
|
+
if (jwt?.exp) {
|
|
31
|
+
const expiresAtMs = jwt.exp * 1000;
|
|
32
|
+
if (Date.now() >= expiresAtMs) {
|
|
33
|
+
return ok(false);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return ok(true);
|
|
37
|
+
}
|
|
38
|
+
async authenticate(provider) {
|
|
39
|
+
// API tokens cannot be obtained automatically — user must provide them.
|
|
40
|
+
return err(new ManualSetupRequired(provider.id, this.setupInstructions));
|
|
41
|
+
}
|
|
42
|
+
async refresh() {
|
|
43
|
+
// Static tokens cannot be refreshed
|
|
44
|
+
return ok(null);
|
|
45
|
+
}
|
|
46
|
+
applyToRequest(credential) {
|
|
47
|
+
if (credential.type !== 'api-key')
|
|
48
|
+
return {};
|
|
49
|
+
const value = this.headerPrefix
|
|
50
|
+
? `${this.headerPrefix} ${credential.key}`
|
|
51
|
+
: credential.key;
|
|
52
|
+
return { [this.headerName]: value };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export class ApiTokenStrategyFactory {
|
|
56
|
+
name = 'api-token';
|
|
57
|
+
create(config) {
|
|
58
|
+
if (config.strategy !== 'api-token') {
|
|
59
|
+
throw new Error(`ApiTokenStrategyFactory received wrong config type: ${config.strategy}`);
|
|
60
|
+
}
|
|
61
|
+
return new ApiTokenStrategy(config);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { IAuthStrategy, IAuthStrategyFactory } from '../core/interfaces/auth-strategy.js';
|
|
2
|
+
import type { StrategyConfig } from '../config/schema.js';
|
|
3
|
+
export declare class BasicAuthStrategyFactory implements IAuthStrategyFactory {
|
|
4
|
+
readonly name = "basic";
|
|
5
|
+
create(config: StrategyConfig): IAuthStrategy;
|
|
6
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ok, err } from '../core/result.js';
|
|
2
|
+
import { ManualSetupRequired } from '../core/errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* Basic authentication strategy.
|
|
5
|
+
* User provides username + password — no browser automation needed.
|
|
6
|
+
* Produces an Authorization: Basic <base64> header.
|
|
7
|
+
*/
|
|
8
|
+
class BasicAuthStrategy {
|
|
9
|
+
setupInstructions;
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.setupInstructions = config.setupInstructions;
|
|
12
|
+
}
|
|
13
|
+
validate(credential) {
|
|
14
|
+
if (credential.type !== 'basic')
|
|
15
|
+
return ok(false);
|
|
16
|
+
return ok(credential.username.length > 0 && credential.password.length > 0);
|
|
17
|
+
}
|
|
18
|
+
async authenticate(provider) {
|
|
19
|
+
return err(new ManualSetupRequired(provider.id, this.setupInstructions ??
|
|
20
|
+
provider.setupInstructions ??
|
|
21
|
+
'Please provide username and password for basic authentication.'));
|
|
22
|
+
}
|
|
23
|
+
async refresh() {
|
|
24
|
+
return ok(null);
|
|
25
|
+
}
|
|
26
|
+
applyToRequest(credential) {
|
|
27
|
+
if (credential.type !== 'basic')
|
|
28
|
+
return {};
|
|
29
|
+
const encoded = Buffer.from(`${credential.username}:${credential.password}`).toString('base64');
|
|
30
|
+
return { Authorization: `Basic ${encoded}` };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class BasicAuthStrategyFactory {
|
|
34
|
+
name = 'basic';
|
|
35
|
+
create(config) {
|
|
36
|
+
if (config.strategy !== 'basic') {
|
|
37
|
+
throw new Error(`BasicAuthStrategyFactory received wrong config type: ${config.strategy}`);
|
|
38
|
+
}
|
|
39
|
+
return new BasicAuthStrategy(config);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { IAuthStrategy, IAuthStrategyFactory } from '../core/interfaces/auth-strategy.js';
|
|
2
|
+
import type { StrategyConfig } from '../config/schema.js';
|
|
3
|
+
export declare class CookieStrategyFactory implements IAuthStrategyFactory {
|
|
4
|
+
readonly name = "cookie";
|
|
5
|
+
create(config: StrategyConfig): IAuthStrategy;
|
|
6
|
+
}
|