diff --git a/backend/internal/bootstrap/app.go b/backend/internal/bootstrap/app.go index a0dbcba..ad8910f 100644 --- a/backend/internal/bootstrap/app.go +++ b/backend/internal/bootstrap/app.go @@ -116,7 +116,7 @@ func NewApp(ctx context.Context) (*App, error) { authSvc := service.NewAuthService(userRepo, siteRepo, sessionSvc, emailCodeSvc, smtpSvc, cgroupRepo) appSettingsSvc := service.NewAppSettingsService(siteRepo, eventRepo, smtpSvc, rustfsClient) imageAccessSvc := service.NewImageAccessService(cfg.GeneratedRoot, showcaseRepo, authSvc) - adobeClient := adobe.NewClient("clio-playground-web", "") + adobeClient := adobe.NewClient("projectx_webapp", "") chatGPTClient := chatgpt.NewClient("") runwayClient := runway.NewClient("") leonardoClient := leonardo.NewClient("") diff --git a/backend/internal/provider/adobe/auth.go b/backend/internal/provider/adobe/auth.go index 7e386f4..603e367 100644 --- a/backend/internal/provider/adobe/auth.go +++ b/backend/internal/provider/adobe/auth.go @@ -9,8 +9,8 @@ import ( const ( refreshURL = "https://adobeid-na1.services.adobe.com/ims/check/v6/token?jslVersion=v2-v0.48.0-1-g1e322cb" - clientID = "clio-playground-web" - scopeValue = "AdobeID,firefly_api,openid,pps.read,pps.write,additional_info.projectedProductContext,additional_info.ownerOrg,uds_read,uds_write,ab.manage,read_organizations,additional_info.roles,account_cluster.read,creative_production,profile" + clientID = "projectx_webapp" + scopeValue = "AdobeID,firefly_api,openid" ) var ErrAdobeCookieEmpty = errors.New("cookie is empty") diff --git a/backend/internal/provider/adobe/client.go b/backend/internal/provider/adobe/client.go index 70810d1..4db0783 100644 --- a/backend/internal/provider/adobe/client.go +++ b/backend/internal/provider/adobe/client.go @@ -58,8 +58,9 @@ var profileURLs = []string{ } type Client struct { - apiKey string - proxy string + apiKey string + proxy string + arpSessionID string // cached per-client, reused across requests (matches adobe2api) } func NewClient(apiKey, proxy string) *Client { @@ -69,6 +70,18 @@ func NewClient(apiKey, proxy string) *Client { } } +// getARPSessionID returns a cached ARP session id matching adobe2api's format: +// base64({"sid":"","ftr":"___dUAL43-mnts-ants-d4_31ck__tt"}) +// Generated once per client and reused — adobe2api reuses the same session id per +// token/profile instead of rotating every request. +func (c *Client) getARPSessionID() string { + if c.arpSessionID != "" { + return c.arpSessionID + } + c.arpSessionID = buildARPSessionID() + return c.arpSessionID +} + func (c *Client) SetProxy(proxy string) { c.proxy = strings.TrimSpace(proxy) } @@ -447,8 +460,8 @@ func (c *Client) submitImage(ctx context.Context, sess *tlsSession, token, promp "x-api-key": {c.apiKey}, "content-type": {"application/json"}, "accept": {"*/*"}, - "origin": {"https://firefly.adobe.com"}, - "referer": {"https://firefly.adobe.com/"}, + "origin": {"https://new.express.adobe.com"}, + "referer": {"https://new.express.adobe.com/"}, "accept-language": {"en-US,en;q=0.9"}, "sec-ch-ua": {sess.fp.secCHUA}, "sec-ch-ua-mobile": {"?0"}, @@ -457,7 +470,7 @@ func (c *Client) submitImage(ctx context.Context, sess *tlsSession, token, promp "sec-fetch-mode": {"cors"}, "sec-fetch-dest": {"empty"}, "user-agent": {sess.fp.userAgent}, - "x-arp-session-id": {buildARPSessionID()}, + "x-arp-session-id": {c.getARPSessionID()}, http.HeaderOrderKey: { "authorization", "x-api-key", @@ -516,16 +529,16 @@ func (c *Client) submitImage(ctx context.Context, sess *tlsSession, token, promp return respBody, "", err } if override := strings.TrimSpace(resp.Header.Get("x-override-status-link")); override != "" { - return respBody, override, nil + return respBody, normalizePollURL(override), nil } if links, ok := payloadResp["links"].(map[string]any); ok { if result, ok := links["result"].(map[string]any); ok { if href := strings.TrimSpace(stringValue(result["href"])); href != "" { - return respBody, href, nil + return respBody, normalizePollURL(href), nil } } if href := strings.TrimSpace(stringValue(links["result"])); href != "" { - return respBody, href, nil + return respBody, normalizePollURL(href), nil } } return respBody, "", errors.New("submit ok but no poll url") @@ -545,8 +558,8 @@ func (c *Client) pollImage(ctx context.Context, sess *tlsSession, token, pollURL req.Header = http.Header{ "authorization": {"Bearer " + strings.TrimSpace(token)}, "accept": {"*/*"}, - "origin": {"https://firefly.adobe.com"}, - "referer": {"https://firefly.adobe.com/"}, + "origin": {"https://new.express.adobe.com"}, + "referer": {"https://new.express.adobe.com/"}, "user-agent": {sess.fp.userAgent}, http.HeaderOrderKey: { "authorization", @@ -625,8 +638,8 @@ func (c *Client) submitVideo(ctx context.Context, sess *tlsSession, token, endpo "x-api-key": {c.apiKey}, "content-type": {"application/json"}, "accept": {"*/*"}, - "origin": {"https://firefly.adobe.com"}, - "referer": {"https://firefly.adobe.com/"}, + "origin": {"https://new.express.adobe.com"}, + "referer": {"https://new.express.adobe.com/"}, "accept-language": {"en-US,en;q=0.9"}, "sec-ch-ua": {sess.fp.secCHUA}, "sec-ch-ua-mobile": {"?0"}, @@ -635,7 +648,7 @@ func (c *Client) submitVideo(ctx context.Context, sess *tlsSession, token, endpo "sec-fetch-mode": {"cors"}, "sec-fetch-dest": {"empty"}, "user-agent": {sess.fp.userAgent}, - "x-arp-session-id": {buildARPSessionID()}, + "x-arp-session-id": {c.getARPSessionID()}, http.HeaderOrderKey: { "authorization", "x-api-key", @@ -700,16 +713,16 @@ func (c *Client) submitVideo(ctx context.Context, sess *tlsSession, token, endpo return respBody, "", err } if override := strings.TrimSpace(resp.Header.Get("x-override-status-link")); override != "" { - return respBody, normalizeVideoPollURL(override), nil + return respBody, normalizePollURL(override), nil } if links, ok := payloadResp["links"].(map[string]any); ok { if result, ok := links["result"].(map[string]any); ok { if href := strings.TrimSpace(stringValue(result["href"])); href != "" { - return respBody, normalizeVideoPollURL(href), nil + return respBody, normalizePollURL(href), nil } } if href := strings.TrimSpace(stringValue(links["result"])); href != "" { - return respBody, normalizeVideoPollURL(href), nil + return respBody, normalizePollURL(href), nil } } return respBody, "", errors.New("video submit ok but no poll url") @@ -729,8 +742,8 @@ func (c *Client) pollVideo(ctx context.Context, sess *tlsSession, token, pollURL req.Header = http.Header{ "authorization": {"Bearer " + strings.TrimSpace(token)}, "accept": {"*/*"}, - "origin": {"https://firefly.adobe.com"}, - "referer": {"https://firefly.adobe.com/"}, + "origin": {"https://new.express.adobe.com"}, + "referer": {"https://new.express.adobe.com/"}, "user-agent": {sess.fp.userAgent}, http.HeaderOrderKey: { "authorization", @@ -949,7 +962,14 @@ func exchangeCookieWithTLSClient(ctx context.Context, sess *tlsSession, cookie s return nil, ErrAdobeCookieEmpty } - body := "client_id=" + clientID + "&guest_allowed=true&scope=" + strings.ReplaceAll(scopeValue, ",", "%2C") + // Prefer user_id (HAR behavior); fall back to guest_allowed for first-time login. + userID := extractUserIDFromCookie(cookie) + var body string + if userID != "" { + body = "client_id=" + clientID + "&scope=" + strings.ReplaceAll(scopeValue, ",", "%2C") + "&user_id=" + url.QueryEscape(userID) + } else { + body = "client_id=" + clientID + "&guest_allowed=true&scope=" + strings.ReplaceAll(scopeValue, ",", "%2C") + } req, err := http.NewRequest(http.MethodPost, refreshURL, strings.NewReader(body)) if err != nil { return nil, err @@ -960,8 +980,8 @@ func exchangeCookieWithTLSClient(ctx context.Context, sess *tlsSession, cookie s "accept-language": {"zh-CN,zh;q=0.9"}, "content-type": {"application/x-www-form-urlencoded;charset=UTF-8"}, "cookie": {cookie}, - "origin": {"https://firefly.adobe.com"}, - "referer": {"https://firefly.adobe.com/"}, + "origin": {"https://new.express.adobe.com"}, + "referer": {"https://new.express.adobe.com/"}, "user-agent": {sess.fp.userAgent}, http.HeaderOrderKey: { "accept", @@ -1033,7 +1053,7 @@ func ExtractAccountID(token string) string { return userID } -func normalizeVideoPollURL(raw string) string { +func normalizePollURL(raw string) string { if strings.TrimSpace(raw) == "" { return raw } diff --git a/backend/internal/provider/adobe/util.go b/backend/internal/provider/adobe/util.go index 5bb4bf4..3a58fcb 100644 --- a/backend/internal/provider/adobe/util.go +++ b/backend/internal/provider/adobe/util.go @@ -6,6 +6,9 @@ import ( "encoding/hex" "encoding/json" "math/big" + "net/url" + "os" + "regexp" "strconv" "strings" "time" @@ -13,6 +16,18 @@ import ( "github.com/google/uuid" ) +// adobeUserIDPat matches Adobe IMS user IDs embedded in cookies (e.g. +// "4BDA81F069FC6DA40A495FAB@AdobeID"). +var adobeUserIDPat = regexp.MustCompile(`[A-Fa-f0-9]{20,}@AdobeID`) + +func extractUserIDFromCookie(cookie string) string { + decoded, err := url.QueryUnescape(cookie) + if err != nil { + decoded = cookie + } + return adobeUserIDPat.FindString(decoded) +} + func stringValue(v any) string { switch x := v.(type) { case string: @@ -82,12 +97,14 @@ func decodeJWTPayload(token string) map[string]any { } func buildARPSessionID() string { - // Every field is randomized per request: no embedded process pid or - // hardcoded constant suffix (those would make all requests from this - // install share a static feature — a cross-account correlation point). + // Matches adobe2api's format exactly: + // base64({"sid":"","ftr":"___dUAL43-mnts-ants-d4_31ck__tt"}) + // Two fields only (no "ark") — mirrors what a real browser session sends. + pid := os.Getpid() + ftr := randomHex(16) + "_" + strconv.FormatInt(time.Now().UnixMilli(), 10) + "_" + strconv.Itoa(pid) + "_dUAL43-mnts-ants-d4_31ck__tt" raw := map[string]any{ "sid": uuid.NewString(), - "ftr": randomHex(16) + "_" + strconv.FormatInt(time.Now().UnixMilli(), 10) + "_" + strconv.Itoa(randomInt(1000, 999999)) + "_" + randomHex(8), + "ftr": ftr, } b, _ := json.Marshal(raw) return base64.StdEncoding.EncodeToString(b)