33 */
44
55import type { Browser , BrowserContext , Page , Response } from 'playwright' ;
6+ import type { z , ZodTypeAny } from 'zod' ;
67import {
78 ApiCredentialStatus ,
89 ApiCredentials ,
@@ -38,6 +39,58 @@ export class LoginFailedError extends Error {
3839 }
3940}
4041
42+ /**
43+ * Thrown when `latchkey auth prepare` is run for a service that does not declare a
44+ * prepare schema (the base default — services opt in by setting one).
45+ */
46+ export class PrepareNotSupportedError extends Error {
47+ constructor ( serviceName : string ) {
48+ super (
49+ `Service '${ serviceName } ' does not support 'latchkey auth prepare'. ` +
50+ `Use 'latchkey services info ${ serviceName } ' to see how to authenticate.`
51+ ) ;
52+ this . name = 'PrepareNotSupportedError' ;
53+ }
54+ }
55+
56+ /**
57+ * Thrown when the JSON passed to `latchkey auth prepare` is malformed or does not
58+ * match the service's prepare schema. The whole command is rejected and
59+ * nothing is stored.
60+ */
61+ export class PrepareInputInvalidError extends Error {
62+ constructor ( serviceName : string , detail : string ) {
63+ super ( `Invalid prepare input for '${ serviceName } ': ${ detail } ` ) ;
64+ this . name = 'PrepareInputInvalidError' ;
65+ }
66+ }
67+
68+ /**
69+ * Validate a parsed JSON value against a service's prepare schema and build the
70+ * resulting credentials. Centralizes validation so each service's
71+ * `prepareFromJson` only expresses its schema and build step. Throws
72+ * `PrepareInputInvalidError` (with the failing fields) on any schema mismatch;
73+ * nothing is built unless the input fully validates.
74+ */
75+ export function buildPreparedCredentials < Schema extends ZodTypeAny > (
76+ serviceName : string ,
77+ schema : Schema ,
78+ parsedJson : unknown ,
79+ build : ( validatedInput : z . infer < Schema > ) => ApiCredentials
80+ ) : ApiCredentials {
81+ const result = schema . safeParse ( parsedJson ) ;
82+ if ( ! result . success ) {
83+ const detail = result . error . issues
84+ . map ( ( issue ) => {
85+ const path = issue . path . join ( '.' ) ;
86+ return path ? `${ path } : ${ issue . message } ` : issue . message ;
87+ } )
88+ . join ( '; ' ) ;
89+ throw new PrepareInputInvalidError ( serviceName , detail ) ;
90+ }
91+ return build ( result . data as z . infer < Schema > ) ;
92+ }
93+
4194export function isBrowserClosedError ( error : Error ) : boolean {
4295 const message = error . message . toLowerCase ( ) ;
4396 return (
@@ -50,6 +103,22 @@ export function isBrowserClosedError(error: Error): boolean {
50103 ) ;
51104}
52105
106+ /**
107+ * Detects the Playwright/CDP error raised when a response body can no longer be
108+ * retrieved (`Network.getResponseBody` reports "No resource with given
109+ * identifier found"). This happens for responses that retain no readable body —
110+ * redirects, evicted or cached resources, or bodies fetched after the page has
111+ * navigated onward. Callers that read response bodies opportunistically should
112+ * treat this as inconclusive rather than fatal.
113+ */
114+ export function isResponseBodyUnavailableError ( error : Error ) : boolean {
115+ const message = error . message . toLowerCase ( ) ;
116+ return (
117+ message . includes ( 'no resource with given identifier' ) ||
118+ message . includes ( 'network.getresponsebody' )
119+ ) ;
120+ }
121+
53122export function isTimeoutError ( error : Error ) : boolean {
54123 return error . name === 'TimeoutError' ;
55124}
@@ -126,6 +195,18 @@ export abstract class Service {
126195 throw new NoCurlCredentialsNotSupportedError ( this . name ) ;
127196 }
128197
198+ /**
199+ * Build credentials from a parsed JSON payload for `latchkey auth prepare`.
200+ *
201+ * Optional, like `getSession`/`refreshCredentials`: services opt in by
202+ * implementing it (typically via `buildPreparedCredentials` with a Zod
203+ * schema). When a service does not implement it, prepare is "not supported"
204+ * — the default that lets every service stay closed until it declares a
205+ * schema. Implementations validate `parsedJson` and throw
206+ * `PrepareInputInvalidError` on mismatch.
207+ */
208+ prepareFromJson ?( parsedJson : unknown ) : ApiCredentials ;
209+
129210 /**
130211 * Get a new session for the login flow.
131212 * Services that don't support browser login should not implement this method.
0 commit comments