signet-auth 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of signet-auth might be problematic. Click here for more details.

Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +393 -0
  3. package/bin/sig.js +65 -0
  4. package/dist/auth-manager.d.ts +90 -0
  5. package/dist/auth-manager.js +262 -0
  6. package/dist/browser/adapters/playwright.adapter.d.ts +14 -0
  7. package/dist/browser/adapters/playwright.adapter.js +188 -0
  8. package/dist/browser/flows/form-login.flow.d.ts +6 -0
  9. package/dist/browser/flows/form-login.flow.js +35 -0
  10. package/dist/browser/flows/header-capture.d.ts +23 -0
  11. package/dist/browser/flows/header-capture.js +104 -0
  12. package/dist/browser/flows/hybrid-flow.d.ts +37 -0
  13. package/dist/browser/flows/hybrid-flow.js +104 -0
  14. package/dist/browser/flows/oauth-consent.flow.d.ts +20 -0
  15. package/dist/browser/flows/oauth-consent.flow.js +170 -0
  16. package/dist/cli/commands/doctor.d.ts +6 -0
  17. package/dist/cli/commands/doctor.js +263 -0
  18. package/dist/cli/commands/get.d.ts +2 -0
  19. package/dist/cli/commands/get.js +83 -0
  20. package/dist/cli/commands/init.d.ts +6 -0
  21. package/dist/cli/commands/init.js +244 -0
  22. package/dist/cli/commands/login.d.ts +2 -0
  23. package/dist/cli/commands/login.js +77 -0
  24. package/dist/cli/commands/logout.d.ts +2 -0
  25. package/dist/cli/commands/logout.js +11 -0
  26. package/dist/cli/commands/providers.d.ts +2 -0
  27. package/dist/cli/commands/providers.js +30 -0
  28. package/dist/cli/commands/remote.d.ts +1 -0
  29. package/dist/cli/commands/remote.js +67 -0
  30. package/dist/cli/commands/request.d.ts +2 -0
  31. package/dist/cli/commands/request.js +82 -0
  32. package/dist/cli/commands/status.d.ts +2 -0
  33. package/dist/cli/commands/status.js +41 -0
  34. package/dist/cli/commands/sync.d.ts +2 -0
  35. package/dist/cli/commands/sync.js +62 -0
  36. package/dist/cli/formatters.d.ts +3 -0
  37. package/dist/cli/formatters.js +25 -0
  38. package/dist/cli/main.d.ts +8 -0
  39. package/dist/cli/main.js +125 -0
  40. package/dist/config/generator.d.ts +24 -0
  41. package/dist/config/generator.js +97 -0
  42. package/dist/config/loader.d.ts +21 -0
  43. package/dist/config/loader.js +54 -0
  44. package/dist/config/schema.d.ts +44 -0
  45. package/dist/config/schema.js +8 -0
  46. package/dist/config/validator.d.ts +15 -0
  47. package/dist/config/validator.js +228 -0
  48. package/dist/core/errors.d.ts +57 -0
  49. package/dist/core/errors.js +107 -0
  50. package/dist/core/interfaces/auth-strategy.d.ts +48 -0
  51. package/dist/core/interfaces/auth-strategy.js +1 -0
  52. package/dist/core/interfaces/browser-adapter.d.ts +73 -0
  53. package/dist/core/interfaces/browser-adapter.js +1 -0
  54. package/dist/core/interfaces/provider.d.ts +15 -0
  55. package/dist/core/interfaces/provider.js +1 -0
  56. package/dist/core/interfaces/storage.d.ts +21 -0
  57. package/dist/core/interfaces/storage.js +1 -0
  58. package/dist/core/result.d.ts +21 -0
  59. package/dist/core/result.js +16 -0
  60. package/dist/core/types.d.ts +128 -0
  61. package/dist/core/types.js +6 -0
  62. package/dist/deps.d.ts +20 -0
  63. package/dist/deps.js +54 -0
  64. package/dist/index.d.ts +35 -0
  65. package/dist/index.js +37 -0
  66. package/dist/providers/auto-provision.d.ts +9 -0
  67. package/dist/providers/auto-provision.js +27 -0
  68. package/dist/providers/config-loader.d.ts +7 -0
  69. package/dist/providers/config-loader.js +7 -0
  70. package/dist/providers/provider-registry.d.ts +19 -0
  71. package/dist/providers/provider-registry.js +68 -0
  72. package/dist/storage/cached-storage.d.ts +24 -0
  73. package/dist/storage/cached-storage.js +57 -0
  74. package/dist/storage/directory-storage.d.ts +25 -0
  75. package/dist/storage/directory-storage.js +184 -0
  76. package/dist/storage/memory-storage.d.ts +14 -0
  77. package/dist/storage/memory-storage.js +27 -0
  78. package/dist/strategies/api-token.strategy.d.ts +6 -0
  79. package/dist/strategies/api-token.strategy.js +63 -0
  80. package/dist/strategies/basic-auth.strategy.d.ts +6 -0
  81. package/dist/strategies/basic-auth.strategy.js +41 -0
  82. package/dist/strategies/cookie.strategy.d.ts +6 -0
  83. package/dist/strategies/cookie.strategy.js +118 -0
  84. package/dist/strategies/oauth2.strategy.d.ts +6 -0
  85. package/dist/strategies/oauth2.strategy.js +134 -0
  86. package/dist/strategies/registry.d.ts +13 -0
  87. package/dist/strategies/registry.js +25 -0
  88. package/dist/sync/remote-config.d.ts +8 -0
  89. package/dist/sync/remote-config.js +49 -0
  90. package/dist/sync/sync-engine.d.ts +10 -0
  91. package/dist/sync/sync-engine.js +96 -0
  92. package/dist/sync/transports/ssh.d.ts +18 -0
  93. package/dist/sync/transports/ssh.js +115 -0
  94. package/dist/sync/types.d.ts +17 -0
  95. package/dist/sync/types.js +1 -0
  96. package/dist/utils/duration.d.ts +9 -0
  97. package/dist/utils/duration.js +34 -0
  98. package/dist/utils/http.d.ts +4 -0
  99. package/dist/utils/http.js +10 -0
  100. package/dist/utils/jwt.d.ts +15 -0
  101. package/dist/utils/jwt.js +30 -0
  102. package/package.json +56 -0
  103. package/src/auth-manager.ts +331 -0
  104. package/src/browser/adapters/playwright.adapter.ts +247 -0
  105. package/src/browser/flows/form-login.flow.ts +35 -0
  106. package/src/browser/flows/header-capture.ts +128 -0
  107. package/src/browser/flows/hybrid-flow.ts +165 -0
  108. package/src/browser/flows/oauth-consent.flow.ts +200 -0
  109. package/src/cli/commands/doctor.ts +301 -0
  110. package/src/cli/commands/get.ts +96 -0
  111. package/src/cli/commands/init.ts +289 -0
  112. package/src/cli/commands/login.ts +94 -0
  113. package/src/cli/commands/logout.ts +17 -0
  114. package/src/cli/commands/providers.ts +39 -0
  115. package/src/cli/commands/remote.ts +71 -0
  116. package/src/cli/commands/request.ts +97 -0
  117. package/src/cli/commands/status.ts +48 -0
  118. package/src/cli/commands/sync.ts +71 -0
  119. package/src/cli/formatters.ts +31 -0
  120. package/src/cli/main.ts +144 -0
  121. package/src/config/generator.ts +122 -0
  122. package/src/config/loader.ts +70 -0
  123. package/src/config/schema.ts +75 -0
  124. package/src/config/validator.ts +281 -0
  125. package/src/core/errors.ts +182 -0
  126. package/src/core/interfaces/auth-strategy.ts +65 -0
  127. package/src/core/interfaces/browser-adapter.ts +81 -0
  128. package/src/core/interfaces/provider.ts +19 -0
  129. package/src/core/interfaces/storage.ts +26 -0
  130. package/src/core/result.ts +24 -0
  131. package/src/core/types.ts +194 -0
  132. package/src/deps.ts +80 -0
  133. package/src/index.ts +109 -0
  134. package/src/providers/auto-provision.ts +30 -0
  135. package/src/providers/config-loader.ts +8 -0
  136. package/src/providers/provider-registry.ts +79 -0
  137. package/src/storage/cached-storage.ts +72 -0
  138. package/src/storage/directory-storage.ts +204 -0
  139. package/src/storage/memory-storage.ts +35 -0
  140. package/src/strategies/api-token.strategy.ts +87 -0
  141. package/src/strategies/basic-auth.strategy.ts +64 -0
  142. package/src/strategies/cookie.strategy.ts +153 -0
  143. package/src/strategies/oauth2.strategy.ts +178 -0
  144. package/src/strategies/registry.ts +34 -0
  145. package/src/sync/remote-config.ts +60 -0
  146. package/src/sync/sync-engine.ts +113 -0
  147. package/src/sync/transports/ssh.ts +130 -0
  148. package/src/sync/types.ts +15 -0
  149. package/src/utils/duration.ts +34 -0
  150. package/src/utils/http.ts +11 -0
  151. package/src/utils/jwt.ts +39 -0
  152. package/tsconfig.json +20 -0
@@ -0,0 +1,10 @@
1
+ import os from 'node:os';
2
+ /**
3
+ * Build a User-Agent string identifying signet.
4
+ */
5
+ export function buildUserAgent() {
6
+ const platform = os.platform();
7
+ const arch = os.arch();
8
+ const nodeVersion = process.version;
9
+ return `signet/1.0.0 (${platform}; ${arch}) Node/${nodeVersion}`;
10
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Lightweight JWT decode without signature verification.
3
+ * Used for reading token expiry and audience — NOT for security validation.
4
+ */
5
+ export interface JwtPayload {
6
+ exp?: number;
7
+ iat?: number;
8
+ aud?: string | string[];
9
+ iss?: string;
10
+ sub?: string;
11
+ [key: string]: unknown;
12
+ }
13
+ export declare function decodeJwt(token: string): JwtPayload | null;
14
+ export declare function isJwtExpired(token: string, bufferMs?: number): boolean;
15
+ export declare function getJwtExpiresAt(token: string): Date | null;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Lightweight JWT decode without signature verification.
3
+ * Used for reading token expiry and audience — NOT for security validation.
4
+ */
5
+ export function decodeJwt(token) {
6
+ try {
7
+ const parts = token.split('.');
8
+ if (parts.length !== 3)
9
+ return null;
10
+ const payload = parts[1];
11
+ const decoded = Buffer.from(payload, 'base64url').toString('utf-8');
12
+ return JSON.parse(decoded);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ export function isJwtExpired(token, bufferMs = 0) {
19
+ const payload = decodeJwt(token);
20
+ if (!payload?.exp)
21
+ return false; // No expiry = assume valid
22
+ const expiresAtMs = payload.exp * 1000;
23
+ return Date.now() + bufferMs >= expiresAtMs;
24
+ }
25
+ export function getJwtExpiresAt(token) {
26
+ const payload = decodeJwt(token);
27
+ if (!payload?.exp)
28
+ return null;
29
+ return new Date(payload.exp * 1000);
30
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "signet-auth",
3
+ "version": "1.0.0-beta.0",
4
+ "description": "General-purpose authentication CLI with pluggable strategies and browser adapters",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "sig": "./bin/sig.js"
9
+ },
10
+ "files": [
11
+ "dist/",
12
+ "bin/",
13
+ "src/",
14
+ "tsconfig.json"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "prepack": "npm run build",
19
+ "start": "node bin/sig.js",
20
+ "dev": "tsc && node bin/sig.js",
21
+ "test": "node --experimental-vm-modules node_modules/.bin/vitest run",
22
+ "test:watch": "node --experimental-vm-modules node_modules/.bin/vitest"
23
+ },
24
+ "dependencies": {
25
+ "playwright-core": "^1.50.0",
26
+ "yaml": "^2.7.0",
27
+ "proper-lockfile": "^4.1.2"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^20.10.0",
31
+ "@types/proper-lockfile": "^4.1.4",
32
+ "typescript": "^5.3.0",
33
+ "vitest": "^3.0.0"
34
+ },
35
+ "keywords": [
36
+ "cli",
37
+ "auth",
38
+ "authentication",
39
+ "oauth2",
40
+ "browser",
41
+ "playwright-core"
42
+ ],
43
+ "author": "pylon <pylon.peng@gmail.com>",
44
+ "license": "MIT",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/pylon/signet.git"
48
+ },
49
+ "engines": {
50
+ "node": ">=18.0.0"
51
+ },
52
+ "publishConfig": {
53
+ "registry": "https://registry.npmjs.org/",
54
+ "access": "public"
55
+ }
56
+ }
@@ -0,0 +1,331 @@
1
+ import type { IAuthStrategy, AuthContext } from './core/interfaces/auth-strategy.js';
2
+ import type { IBrowserAdapter } from './core/interfaces/browser-adapter.js';
3
+ import type { IStorage } from './core/interfaces/storage.js';
4
+ import type { IProviderRegistry } from './core/interfaces/provider.js';
5
+ import type { Credential, ProviderConfig, StoredCredential, ProviderStatus, ILogger } from './core/types.js';
6
+ import type { BrowserConfig } from './config/schema.js';
7
+ import { createDefaultProvider } from './providers/auto-provision.js';
8
+ import type { Result } from './core/result.js';
9
+ import { ok, err, isOk } from './core/result.js';
10
+ import {
11
+ CredentialNotFoundError,
12
+ CredentialExpiredError,
13
+ CredentialTypeError,
14
+ ProviderNotFoundError,
15
+ type AuthError,
16
+ } from './core/errors.js';
17
+ import { StrategyRegistry } from './strategies/registry.js';
18
+
19
+ export interface AuthManagerDeps {
20
+ storage: IStorage;
21
+ strategyRegistry: StrategyRegistry;
22
+ providerRegistry: IProviderRegistry;
23
+ browserAdapterFactory: () => IBrowserAdapter;
24
+ browserConfig: BrowserConfig;
25
+ logger?: ILogger;
26
+ }
27
+
28
+ /**
29
+ * Central orchestrator for authentication lifecycle.
30
+ * All dependencies are injected — no singletons, no global state.
31
+ *
32
+ * Flow: validate → refresh → authenticate
33
+ */
34
+ export class AuthManager {
35
+ private readonly storage: IStorage;
36
+ private readonly strategies: StrategyRegistry;
37
+ private readonly providers: IProviderRegistry;
38
+ private readonly browserAdapterFactory: () => IBrowserAdapter;
39
+ private readonly browserConfig: BrowserConfig;
40
+ private readonly logger?: ILogger;
41
+
42
+ constructor(deps: AuthManagerDeps) {
43
+ this.storage = deps.storage;
44
+ this.strategies = deps.strategyRegistry;
45
+ this.providers = deps.providerRegistry;
46
+ this.browserAdapterFactory = deps.browserAdapterFactory;
47
+ this.browserConfig = deps.browserConfig;
48
+ this.logger = deps.logger;
49
+ }
50
+
51
+ /**
52
+ * Get valid credentials for a provider.
53
+ * Tries: stored → refresh → authenticate, in that order.
54
+ */
55
+ async getCredentials(providerId: string): Promise<Result<Credential, AuthError>> {
56
+ const provider = this.providers.get(providerId);
57
+ if (!provider) return err(new ProviderNotFoundError(providerId));
58
+
59
+ const strategy = this.strategies.get(provider.strategy, provider.strategyConfig);
60
+ const key = this.storageKey(provider);
61
+
62
+ // Step 1: Check stored credentials
63
+ const stored = await this.storage.get(key);
64
+ if (stored) {
65
+ const validation = strategy.validate(stored.credential, provider.strategyConfig);
66
+ if (isOk(validation) && validation.value) {
67
+ // Check credential type constraints
68
+ const typeCheck = this.checkCredentialType(provider, stored.credential);
69
+ if (!isOk(typeCheck)) return typeCheck;
70
+ return ok(stored.credential);
71
+ }
72
+
73
+ // Step 2: Try refresh
74
+ this.logger?.debug(`Credentials for "${providerId}" are invalid, attempting refresh...`);
75
+ const refreshResult = await strategy.refresh(stored.credential, provider.strategyConfig);
76
+ if (isOk(refreshResult) && refreshResult.value) {
77
+ const typeCheck = this.checkCredentialType(provider, refreshResult.value);
78
+ if (!isOk(typeCheck)) return typeCheck;
79
+
80
+ await this.store(key, provider.strategy, refreshResult.value);
81
+ return ok(refreshResult.value);
82
+ }
83
+ }
84
+
85
+ // Step 3: Full authentication
86
+ this.logger?.info(`Authenticating with "${providerId}"...`);
87
+ const context: AuthContext = {
88
+ browserAdapter: this.browserAdapterFactory(),
89
+ browserConfig: this.browserConfig,
90
+ logger: this.logger,
91
+ };
92
+
93
+ const authResult = await strategy.authenticate(provider, context);
94
+ if (!isOk(authResult)) return authResult;
95
+
96
+ const typeCheck = this.checkCredentialType(provider, authResult.value);
97
+ if (!isOk(typeCheck)) return typeCheck;
98
+
99
+ await this.store(key, provider.strategy, authResult.value);
100
+ return ok(authResult.value);
101
+ }
102
+
103
+ /**
104
+ * Resolve a provider by URL, auto-provisioning a default cookie provider if none matches.
105
+ */
106
+ resolveProvider(url: string): ProviderConfig {
107
+ const existing = this.providers.resolve(url);
108
+ if (existing) return existing;
109
+
110
+ const provider = createDefaultProvider(url);
111
+ this.providers.register(provider);
112
+ this.logger?.info(`Auto-provisioned provider "${provider.id}" for ${url}`);
113
+ return provider;
114
+ }
115
+
116
+ /**
117
+ * Get credentials for a specific provider, resolving by URL.
118
+ */
119
+ async getCredentialsByUrl(url: string): Promise<Result<{ provider: ProviderConfig; credential: Credential }, AuthError>> {
120
+ const provider = this.resolveProvider(url);
121
+
122
+ const result = await this.getCredentials(provider.id);
123
+ if (!isOk(result)) return result;
124
+
125
+ return ok({ provider, credential: result.value });
126
+ }
127
+
128
+ /**
129
+ * Force re-authentication, deleting any stored credentials first.
130
+ */
131
+ async forceReauth(providerId: string): Promise<Result<Credential, AuthError>> {
132
+ const provider = this.providers.get(providerId);
133
+ if (provider) {
134
+ await this.storage.delete(this.storageKey(provider));
135
+ }
136
+ return this.getCredentials(providerId);
137
+ }
138
+
139
+ /**
140
+ * Store a credential directly (e.g., user-provided API token).
141
+ */
142
+ async setCredential(
143
+ providerId: string,
144
+ credential: Credential,
145
+ ): Promise<Result<void, AuthError>> {
146
+ const provider = this.providers.get(providerId);
147
+ if (!provider) return err(new ProviderNotFoundError(providerId));
148
+
149
+ const typeCheck = this.checkCredentialType(provider, credential);
150
+ if (!isOk(typeCheck)) return typeCheck;
151
+
152
+ await this.store(this.storageKey(provider), provider.strategy, credential);
153
+ return ok(undefined);
154
+ }
155
+
156
+ /**
157
+ * Get status for a provider (non-triggering — won't start auth).
158
+ */
159
+ async getStatus(providerId: string): Promise<ProviderStatus> {
160
+ const provider = this.providers.get(providerId);
161
+ if (!provider) {
162
+ return {
163
+ id: providerId,
164
+ name: providerId,
165
+ configured: false,
166
+ valid: false,
167
+ strategy: 'unknown',
168
+ };
169
+ }
170
+
171
+ const stored = await this.storage.get(this.storageKey(provider));
172
+ if (!stored) {
173
+ return {
174
+ id: provider.id,
175
+ name: provider.name,
176
+ configured: true,
177
+ valid: false,
178
+ strategy: provider.strategy,
179
+ };
180
+ }
181
+
182
+ const strategy = this.strategies.get(provider.strategy, provider.strategyConfig);
183
+ const validation = strategy.validate(stored.credential, provider.strategyConfig);
184
+ const valid = isOk(validation) && validation.value;
185
+
186
+ const expiresAt = this.getExpiresAt(stored.credential);
187
+ const expiresInMinutes = expiresAt
188
+ ? Math.max(0, Math.round((expiresAt.getTime() - Date.now()) / 60000))
189
+ : undefined;
190
+
191
+ return {
192
+ id: provider.id,
193
+ name: provider.name,
194
+ configured: true,
195
+ valid,
196
+ credentialType: stored.credential.type,
197
+ strategy: provider.strategy,
198
+ expiresAt: expiresAt?.toISOString(),
199
+ expiresInMinutes,
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Get status for all configured providers.
205
+ */
206
+ async getAllStatus(): Promise<ProviderStatus[]> {
207
+ const providers = this.providers.list();
208
+ return Promise.all(providers.map(p => this.getStatus(p.id)));
209
+ }
210
+
211
+ /**
212
+ * Clear stored credentials for a provider.
213
+ */
214
+ async clearCredentials(providerId: string): Promise<void> {
215
+ const provider = this.providers.get(providerId);
216
+ const key = provider ? this.storageKey(provider) : providerId;
217
+ await this.storage.delete(key);
218
+ }
219
+
220
+ /**
221
+ * Clear all stored credentials.
222
+ */
223
+ async clearAll(): Promise<void> {
224
+ await this.storage.clear();
225
+ }
226
+
227
+ /**
228
+ * Apply credentials to an outgoing request (as headers).
229
+ */
230
+ applyToRequest(
231
+ providerId: string,
232
+ credential: Credential,
233
+ ): Record<string, string> {
234
+ const provider = this.providers.get(providerId);
235
+ if (!provider) return {};
236
+
237
+ const strategy = this.strategies.get(provider.strategy, provider.strategyConfig);
238
+ return strategy.applyToRequest(credential);
239
+ }
240
+
241
+ /**
242
+ * Validate a credential by making a test request to the provider's entry URL.
243
+ * Returns the HTTP status and whether the response redirects to a login page.
244
+ */
245
+ async validateCredential(
246
+ provider: ProviderConfig,
247
+ credential: Credential,
248
+ ): Promise<{ status: number | null; isLoginRedirect: boolean }> {
249
+ if (!provider.entryUrl) return { status: null, isLoginRedirect: false };
250
+ try {
251
+ const strategy = this.strategies.get(provider.strategy, provider.strategyConfig);
252
+ const headers = strategy.applyToRequest(credential);
253
+ const response = await fetch(provider.entryUrl, {
254
+ method: 'GET',
255
+ headers: { ...headers, 'User-Agent': 'signet/1.0' },
256
+ redirect: 'manual',
257
+ });
258
+ const location = response.headers.get('location') ?? '';
259
+ const loginPatterns = [
260
+ '/login', '/signin', '/sign-in', '/auth', '/sso', '/oauth',
261
+ '/adfs/', '/saml/', 'login.microsoftonline.com',
262
+ 'accounts.google.com',
263
+ ];
264
+ const isLoginRedirect = response.status >= 300 && response.status < 400
265
+ && loginPatterns.some(p => location.toLowerCase().includes(p));
266
+ return { status: response.status, isLoginRedirect };
267
+ } catch {
268
+ return { status: null, isLoginRedirect: false };
269
+ }
270
+ }
271
+
272
+ /** Expose the provider registry for handlers */
273
+ get providerRegistry(): IProviderRegistry {
274
+ return this.providers;
275
+ }
276
+
277
+ /** Storage key: uses credentialFile if configured, otherwise provider ID. */
278
+ private storageKey(provider: ProviderConfig): string {
279
+ return provider.credentialFile ?? provider.id;
280
+ }
281
+
282
+ private checkCredentialType(
283
+ provider: ProviderConfig,
284
+ credential: Credential,
285
+ ): Result<void, AuthError> {
286
+ if (
287
+ provider.acceptedCredentialTypes &&
288
+ provider.acceptedCredentialTypes.length > 0 &&
289
+ !provider.acceptedCredentialTypes.includes(credential.type)
290
+ ) {
291
+ return err(new CredentialTypeError(
292
+ provider.id,
293
+ provider.acceptedCredentialTypes,
294
+ credential.type,
295
+ ));
296
+ }
297
+ return ok(undefined);
298
+ }
299
+
300
+ private async store(
301
+ providerId: string,
302
+ strategy: string,
303
+ credential: Credential,
304
+ ): Promise<void> {
305
+ // Strip transient diagnostics metadata before persisting
306
+ const { __diagnostics, ...clean } = credential as any;
307
+ const stored: StoredCredential = {
308
+ credential: clean,
309
+ providerId,
310
+ strategy,
311
+ updatedAt: new Date().toISOString(),
312
+ };
313
+ await this.storage.set(providerId, stored);
314
+ }
315
+
316
+ private getExpiresAt(credential: Credential): Date | null {
317
+ switch (credential.type) {
318
+ case 'bearer':
319
+ return credential.expiresAt ? new Date(credential.expiresAt) : null;
320
+ case 'cookie': {
321
+ // Earliest cookie expiry, or null if all session cookies
322
+ const expiries = credential.cookies
323
+ .filter(c => c.expires > 0)
324
+ .map(c => c.expires * 1000);
325
+ return expiries.length > 0 ? new Date(Math.min(...expiries)) : null;
326
+ }
327
+ default:
328
+ return null;
329
+ }
330
+ }
331
+ }
@@ -0,0 +1,247 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import type {
4
+ IBrowserAdapter,
5
+ IBrowserSession,
6
+ IBrowserPage,
7
+ NavigateOptions,
8
+ PageRequest,
9
+ PageResponse,
10
+ } from "../../core/interfaces/browser-adapter.js";
11
+ import type { Cookie, BrowserLaunchOptions } from "../../core/types.js";
12
+ import type { BrowserConfig } from "../../config/schema.js";
13
+ import { BrowserLaunchError } from "../../core/errors.js";
14
+
15
+ function expandHome(p: string): string {
16
+ if (p.startsWith("~/") || p === "~") {
17
+ return path.join(os.homedir(), p.slice(1));
18
+ }
19
+ return p;
20
+ }
21
+
22
+ /**
23
+ * Playwright-based browser adapter.
24
+ * Uses playwright-core (no bundled browsers) — requires a system Chrome/Chromium/Edge.
25
+ * Defaults to system Chrome (channel: 'chrome') when no explicit browser is configured.
26
+ */
27
+ export class PlaywrightAdapter implements IBrowserAdapter {
28
+ readonly name = "playwright";
29
+
30
+ constructor(private readonly browserConfig: BrowserConfig) {}
31
+
32
+ async launch(options: BrowserLaunchOptions): Promise<IBrowserSession> {
33
+ const { browserDataDir, channel } = this.browserConfig;
34
+
35
+ let pw: typeof import("playwright-core");
36
+ try {
37
+ pw = await import("playwright-core");
38
+ } catch {
39
+ throw new BrowserLaunchError(
40
+ "playwright-core is not available. Run: npm install playwright-core",
41
+ );
42
+ }
43
+
44
+ try {
45
+ const context = await pw.chromium.launchPersistentContext(
46
+ expandHome(browserDataDir),
47
+ {
48
+ channel: channel,
49
+ headless: options.headless ?? true,
50
+ timeout: options.timeout,
51
+ args: options.args,
52
+ },
53
+ );
54
+ return new PlaywrightSession(context);
55
+ } catch (e: unknown) {
56
+ const msg = (e as Error).message;
57
+ const hint =
58
+ msg.includes("executable") || msg.includes("Failed to launch")
59
+ ? `${msg}. Ensure a system browser is installed, or check browser.channel in ~/.signet/config.yaml.`
60
+ : msg;
61
+ throw new BrowserLaunchError(hint);
62
+ }
63
+ }
64
+ }
65
+
66
+ class PlaywrightSession implements IBrowserSession {
67
+ constructor(
68
+ private readonly context: import("playwright-core").BrowserContext,
69
+ ) {}
70
+
71
+ async newPage(): Promise<IBrowserPage> {
72
+ const page = await this.context.newPage();
73
+ return new PlaywrightPage(page, this.context);
74
+ }
75
+
76
+ async pages(): Promise<IBrowserPage[]> {
77
+ return this.context.pages().map((p) => new PlaywrightPage(p, this.context));
78
+ }
79
+
80
+ async close(): Promise<void> {
81
+ try {
82
+ await this.context.close();
83
+ } catch {
84
+ // Suppress close errors
85
+ }
86
+ }
87
+
88
+ isConnected(): boolean {
89
+ return true; // Persistent context doesn't expose isConnected
90
+ }
91
+ }
92
+
93
+ class PlaywrightPage implements IBrowserPage {
94
+ constructor(
95
+ private readonly page: import("playwright-core").Page,
96
+ private readonly context: import("playwright-core").BrowserContext,
97
+ ) {}
98
+
99
+ async goto(url: string, options?: NavigateOptions): Promise<void> {
100
+ await this.page.goto(url, {
101
+ waitUntil: options?.waitUntil as
102
+ | "load"
103
+ | "networkidle"
104
+ | "domcontentloaded"
105
+ | "commit",
106
+ timeout: options?.timeout,
107
+ });
108
+ }
109
+
110
+ url(): string {
111
+ return this.page.url();
112
+ }
113
+
114
+ async waitForUrl(
115
+ pattern: string | RegExp,
116
+ options?: { timeout?: number },
117
+ ): Promise<void> {
118
+ await this.page.waitForURL(pattern, options);
119
+ }
120
+
121
+ async waitForNavigation(options?: { timeout?: number }): Promise<void> {
122
+ await this.page.waitForLoadState("networkidle", options);
123
+ }
124
+
125
+ async waitForLoadState(
126
+ state?: "load" | "networkidle" | "domcontentloaded",
127
+ ): Promise<void> {
128
+ await this.page.waitForLoadState(state);
129
+ }
130
+
131
+ async fill(selector: string, value: string): Promise<void> {
132
+ await this.page.locator(selector).fill(value);
133
+ }
134
+
135
+ async click(selector: string, options?: { timeout?: number }): Promise<void> {
136
+ await this.page.locator(selector).click({ timeout: options?.timeout });
137
+ }
138
+
139
+ async type(
140
+ selector: string,
141
+ text: string,
142
+ options?: { delay?: number },
143
+ ): Promise<void> {
144
+ await this.page
145
+ .locator(selector)
146
+ .pressSequentially(text, { delay: options?.delay });
147
+ }
148
+
149
+ async waitForSelector(
150
+ selector: string,
151
+ options?: { timeout?: number; state?: "visible" | "hidden" | "attached" },
152
+ ): Promise<void> {
153
+ await this.page.locator(selector).waitFor({
154
+ timeout: options?.timeout,
155
+ state: options?.state,
156
+ });
157
+ }
158
+
159
+ async cookies(urls?: string[]): Promise<Cookie[]> {
160
+ const rawCookies = await this.context.cookies(urls);
161
+ return rawCookies.map((c) => ({
162
+ name: c.name,
163
+ value: c.value,
164
+ domain: c.domain,
165
+ path: c.path,
166
+ expires: c.expires,
167
+ httpOnly: c.httpOnly,
168
+ secure: c.secure,
169
+ sameSite:
170
+ c.sameSite === "Strict"
171
+ ? "Strict"
172
+ : c.sameSite === "Lax"
173
+ ? "Lax"
174
+ : c.sameSite === "None"
175
+ ? "None"
176
+ : undefined,
177
+ }));
178
+ }
179
+
180
+ async evaluate<T>(fn: (() => T) | string): Promise<T> {
181
+ if (typeof fn === "string") {
182
+ return (await this.page.evaluate(fn)) as T;
183
+ }
184
+ return await this.page.evaluate(fn);
185
+ }
186
+
187
+ async evaluateWithArg<T, A>(fn: (arg: A) => T, arg: A): Promise<T> {
188
+ return await this.page.evaluate(fn as any, arg as any);
189
+ }
190
+
191
+ async screenshot(options?: {
192
+ path?: string;
193
+ fullPage?: boolean;
194
+ }): Promise<Buffer> {
195
+ return Buffer.from(await this.page.screenshot(options));
196
+ }
197
+
198
+ async content(): Promise<string> {
199
+ return await this.page.content();
200
+ }
201
+
202
+ async title(): Promise<string> {
203
+ return await this.page.title();
204
+ }
205
+
206
+ async close(): Promise<void> {
207
+ if (!this.page.isClosed()) {
208
+ await this.page.close();
209
+ }
210
+ }
211
+
212
+ isClosed(): boolean {
213
+ return this.page.isClosed();
214
+ }
215
+
216
+ onClose(handler: () => void): void {
217
+ this.page.on("close", handler);
218
+ }
219
+
220
+ onRequest(handler: (request: PageRequest) => void): () => void {
221
+ const listener = (req: import("playwright-core").Request) => {
222
+ handler({
223
+ url: req.url(),
224
+ method: req.method(),
225
+ headers: req.headers(),
226
+ });
227
+ };
228
+ this.page.on("request", listener);
229
+ return () => {
230
+ this.page.off("request", listener);
231
+ };
232
+ }
233
+
234
+ onResponse(handler: (response: PageResponse) => void): () => void {
235
+ const listener = (res: import("playwright-core").Response) => {
236
+ handler({
237
+ url: res.url(),
238
+ status: res.status(),
239
+ headers: res.headers(),
240
+ });
241
+ };
242
+ this.page.on("response", listener);
243
+ return () => {
244
+ this.page.off("response", listener);
245
+ };
246
+ }
247
+ }