refactor(adobe): switch to express.adobe.com origin, remove ErrAuthPermanent, simplify ARP session/PID

This commit is contained in:
2026-08-02 00:07:17 +08:00
parent 0a1811b254
commit cd0b7090f2
4 changed files with 47 additions and 133 deletions
+34 -81
View File
@@ -34,7 +34,6 @@ const (
var ( var (
ErrAuth = errors.New("adobe auth failed") ErrAuth = errors.New("adobe auth failed")
ErrAuthPermanent = errors.New("adobe auth permanently failed")
ErrQuotaExhausted = errors.New("adobe quota exhausted") ErrQuotaExhausted = errors.New("adobe quota exhausted")
ErrTemporaryUpstream = errors.New("adobe upstream temporary error") ErrTemporaryUpstream = errors.New("adobe upstream temporary error")
ErrDeadUpstream = errors.New("adobe upstream fatal error") ErrDeadUpstream = errors.New("adobe upstream fatal error")
@@ -53,13 +52,6 @@ func isContentRejection(status int, body string) bool {
return status == 451 && strings.Contains(body, "unsafe") return status == 451 && strings.Contains(body, "unsafe")
} }
func isPermanentAuthError(header string, body []byte) bool {
return strings.EqualFold(header, "user_not_entitled") ||
strings.EqualFold(header, "access_error") ||
strings.Contains(string(body), "user_not_entitled") ||
strings.Contains(string(body), "access_error")
}
var profileURLs = []string{ var profileURLs = []string{
"https://ims-na1.adobelogin.com/ims/profile/v1", "https://ims-na1.adobelogin.com/ims/profile/v1",
"https://adobeid-na1.services.adobe.com/ims/profile/v1", "https://adobeid-na1.services.adobe.com/ims/profile/v1",
@@ -68,6 +60,7 @@ var profileURLs = []string{
type Client struct { type Client struct {
apiKey string apiKey string
proxy string proxy string
arpSessionID string // cached per-client, reused across requests (matches adobe2api)
} }
func NewClient(apiKey, proxy string) *Client { func NewClient(apiKey, proxy string) *Client {
@@ -77,6 +70,17 @@ func NewClient(apiKey, proxy string) *Client {
} }
} }
// getARPSessionID returns a cached ARP session id matching adobe2api's format:
// base64({"sid":"<uuid>","ftr":"<hex16>_<ts_ms>_<pid>_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) { func (c *Client) SetProxy(proxy string) {
c.proxy = strings.TrimSpace(proxy) c.proxy = strings.TrimSpace(proxy)
@@ -93,7 +97,7 @@ func (c *Client) ExchangeCookie(ctx context.Context, cookie string) (*CookieExch
// uploadMaxRetries is how many extra in-place attempts a transient upload // uploadMaxRetries is how many extra in-place attempts a transient upload
// failure (transport error / timeout, 429/451/5xx) gets on a fresh connection // failure (transport error / timeout, 429/451/5xx) gets on a fresh connection
// before the error is surfaced. // before the error is surfaced.
const uploadMaxRetries = 3 const uploadMaxRetries = 5
// UploadImage stores a reference image and returns its blob id. Transient // UploadImage stores a reference image and returns its blob id. Transient
// failures are retried in place (uploadMaxRetries times); the final error keeps // failures are retried in place (uploadMaxRetries times); the final error keeps
@@ -103,13 +107,6 @@ func (c *Client) UploadImage(ctx context.Context, token string, content []byte,
// Reference-image upload runs on the local IP (not the proxy). // Reference-image upload runs on the local IP (not the proxy).
body, err, retryable := c.uploadImageOnce(ctx, token, content, contentType, engine) body, err, retryable := c.uploadImageOnce(ctx, token, content, contentType, engine)
for attempt := 0; err != nil && retryable && attempt < uploadMaxRetries && ctx.Err() == nil; attempt++ { for attempt := 0; err != nil && retryable && attempt < uploadMaxRetries && ctx.Err() == nil; attempt++ {
if wait := time.Duration(attempt+1) * time.Second; wait > 0 {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(wait):
}
}
body, err, retryable = c.uploadImageOnce(ctx, token, content, contentType, engine) body, err, retryable = c.uploadImageOnce(ctx, token, content, contentType, engine)
} }
if err != nil { if err != nil {
@@ -137,7 +134,7 @@ func (c *Client) UploadImage(ctx context.Context, token string, content []byte,
// body plus whether a failure is retryable (transport error / 429/451/5xx). // body plus whether a failure is retryable (transport error / 429/451/5xx).
// Auth failures (401/403) and other non-200s are not retryable. // Auth failures (401/403) and other non-200s are not retryable.
func (c *Client) uploadImageOnce(ctx context.Context, token string, content []byte, contentType, engine string) ([]byte, error, bool) { func (c *Client) uploadImageOnce(ctx context.Context, token string, content []byte, contentType, engine string) ([]byte, error, bool) {
sess, err := c.newUploadTLSClient() sess, err := c.newDirectTLSClient()
if err != nil { if err != nil {
return nil, err, false return nil, err, false
} }
@@ -179,9 +176,6 @@ func (c *Client) uploadImageOnce(ctx context.Context, token string, content []by
return nil, err, true return nil, err, true
} }
if resp.StatusCode == 401 || resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
if isPermanentAuthError(resp.Header.Get("x-access-error"), body) {
return nil, fmt.Errorf("%w (upload %d %s: %s)", ErrAuthPermanent, resp.StatusCode, resp.Header.Get("x-access-error"), clip(body, 300)), false
}
return nil, fmt.Errorf("%w (upload %d %s: %s)", ErrAuth, resp.StatusCode, resp.Header.Get("x-access-error"), clip(body, 300)), false return nil, fmt.Errorf("%w (upload %d %s: %s)", ErrAuth, resp.StatusCode, resp.Header.Get("x-access-error"), clip(body, 300)), false
} }
if resp.StatusCode == 429 || resp.StatusCode == 451 || resp.StatusCode >= 500 { if resp.StatusCode == 429 || resp.StatusCode == 451 || resp.StatusCode >= 500 {
@@ -195,19 +189,15 @@ func (c *Client) uploadImageOnce(ctx context.Context, token string, content []by
func (c *Client) GenerateImage(ctx context.Context, token, modelID, prompt, aspectRatio, resolution string, blobIDs []string, downloadResult bool) ([]byte, map[string]any, error) { func (c *Client) GenerateImage(ctx context.Context, token, modelID, prompt, aspectRatio, resolution string, blobIDs []string, downloadResult bool) ([]byte, map[string]any, error) {
// Only the generate submit goes through the proxy; polling + download run on // Only the generate submit goes through the proxy; polling + download run on
// the local IP. If the proxy connection fails, retry once on the local IP. // the local IP.
submitSess, err := c.newTLSClient() submitSess, err := c.newTLSClient()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
directSess, directErr := c.newDirectTLSClient()
pollSess, err := c.newDirectTLSClient() pollSess, err := c.newDirectTLSClient()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if directErr != nil {
directSess = pollSess
}
var lastBody []byte var lastBody []byte
var lastErr error var lastErr error
@@ -223,9 +213,6 @@ func (c *Client) GenerateImage(ctx context.Context, token, modelID, prompt, aspe
} }
for _, payload := range candidates { for _, payload := range candidates {
respBody, pollURL, err := c.submitImage(ctx, submitSess, token, prompt, endpoint, payload) respBody, pollURL, err := c.submitImage(ctx, submitSess, token, prompt, endpoint, payload)
if errors.Is(err, ErrTemporaryUpstream) {
respBody, pollURL, err = c.submitImage(ctx, directSess, token, prompt, endpoint, payload)
}
if err == nil { if err == nil {
meta, data, pollErr := c.pollImage(ctx, pollSess, token, pollURL, downloadResult) meta, data, pollErr := c.pollImage(ctx, pollSess, token, pollURL, downloadResult)
if pollErr != nil { if pollErr != nil {
@@ -235,7 +222,7 @@ func (c *Client) GenerateImage(ctx context.Context, token, modelID, prompt, aspe
} }
lastBody = respBody lastBody = respBody
lastErr = err lastErr = err
if errors.Is(err, ErrAuth) || errors.Is(err, ErrAuthPermanent) || errors.Is(err, ErrQuotaExhausted) || errors.Is(err, ErrContentRejected) { if errors.Is(err, ErrAuth) || errors.Is(err, ErrQuotaExhausted) || errors.Is(err, ErrContentRejected) {
return nil, nil, err return nil, nil, err
} }
} }
@@ -265,14 +252,10 @@ func (c *Client) GenerateVideo(ctx context.Context, token, engine, prompt, aspec
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
directSess, directErr := c.newDirectTLSClient()
pollSess, err := c.newDirectTLSClient() pollSess, err := c.newDirectTLSClient()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if directErr != nil {
directSess = pollSess
}
payload := BuildVideoPayload(engine, prompt, aspectRatio, durationSeconds, resolution, referenceMode, upstreamModel, blobIDs) payload := BuildVideoPayload(engine, prompt, aspectRatio, durationSeconds, resolution, referenceMode, upstreamModel, blobIDs)
endpoint := videoSubmitURL endpoint := videoSubmitURL
@@ -280,9 +263,6 @@ func (c *Client) GenerateVideo(ctx context.Context, token, engine, prompt, aspec
endpoint = fireflyVideoSubmitURL endpoint = fireflyVideoSubmitURL
} }
respBody, pollURL, err := c.submitVideo(ctx, submitSess, token, endpoint, payload) respBody, pollURL, err := c.submitVideo(ctx, submitSess, token, endpoint, payload)
if errors.Is(err, ErrTemporaryUpstream) {
respBody, pollURL, err = c.submitVideo(ctx, directSess, token, endpoint, payload)
}
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -320,7 +300,6 @@ func (c *Client) FetchAccountProfile(ctx context.Context, token string) (map[str
"user-agent", "user-agent",
}, },
} }
defer ReleasePID(token)
resp, err := sess.client.Do(req) resp, err := sess.client.Do(req)
if err != nil { if err != nil {
@@ -429,9 +408,6 @@ func (c *Client) FetchCreditsBalance(ctx context.Context, token string) (map[str
return nil, err return nil, err
} }
if resp.StatusCode == 401 { if resp.StatusCode == 401 {
if isPermanentAuthError(resp.Header.Get("x-access-error"), body) {
return nil, ErrAuthPermanent
}
return nil, ErrAuth return nil, ErrAuth
} }
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
@@ -484,17 +460,16 @@ func (c *Client) submitImage(ctx context.Context, sess *tlsSession, token, promp
"x-api-key": {c.apiKey}, "x-api-key": {c.apiKey},
"content-type": {"application/json"}, "content-type": {"application/json"},
"accept": {"*/*"}, "accept": {"*/*"},
"origin": {"https://firefly.adobe.com"}, "origin": {"https://new.express.adobe.com"},
"referer": {"https://firefly.adobe.com/"}, "referer": {"https://new.express.adobe.com/"},
"accept-language": {"en-US,en;q=0.9"}, "accept-language": {"en-US,en;q=0.9"},
"sec-ch-ua": {sess.fp.secCHUA}, "sec-ch-ua": {sess.fp.secCHUA},
"sec-ch-ua-mobile": {"?0"}, "sec-ch-ua-mobile": {"?0"},
"sec-ch-ua-platform": {sess.fp.platform}, "sec-ch-ua-platform": {sess.fp.platform},
"sec-fetch-site": {"same-site"}, "sec-fetch-site": {"cross-site"},
"sec-fetch-mode": {"cors"}, "sec-fetch-mode": {"cors"},
"sec-fetch-dest": {"empty"}, "sec-fetch-dest": {"empty"},
"user-agent": {sess.fp.userAgent}, "user-agent": {sess.fp.userAgent},
"x-arp-session-id": {buildARPSessionID(token)},
http.HeaderOrderKey: { http.HeaderOrderKey: {
"authorization", "authorization",
"x-api-key", "x-api-key",
@@ -511,7 +486,6 @@ func (c *Client) submitImage(ctx context.Context, sess *tlsSession, token, promp
"sec-fetch-dest", "sec-fetch-dest",
"user-agent", "user-agent",
"x-nonce", "x-nonce",
"x-arp-session-id",
}, },
} }
if nonce := buildSubmitNonce(token, prompt); nonce != "" { if nonce := buildSubmitNonce(token, prompt); nonce != "" {
@@ -531,9 +505,6 @@ func (c *Client) submitImage(ctx context.Context, sess *tlsSession, token, promp
if strings.EqualFold(resp.Header.Get("x-access-error"), "taste_exhausted") { if strings.EqualFold(resp.Header.Get("x-access-error"), "taste_exhausted") {
return respBody, "", ErrQuotaExhausted return respBody, "", ErrQuotaExhausted
} }
if isPermanentAuthError(resp.Header.Get("x-access-error"), respBody) {
return respBody, "", fmt.Errorf("%w (submit %d %s: %s)", ErrAuthPermanent, resp.StatusCode, resp.Header.Get("x-access-error"), clip(respBody, 300))
}
return respBody, "", fmt.Errorf("%w (submit %d %s: %s)", ErrAuth, resp.StatusCode, resp.Header.Get("x-access-error"), clip(respBody, 300)) return respBody, "", fmt.Errorf("%w (submit %d %s: %s)", ErrAuth, resp.StatusCode, resp.Header.Get("x-access-error"), clip(respBody, 300))
} }
// "system under load" / timeout_error = adobe rate-limit/overload (can come on a // "system under load" / timeout_error = adobe rate-limit/overload (can come on a
@@ -547,9 +518,6 @@ func (c *Client) submitImage(ctx context.Context, sess *tlsSession, token, promp
if resp.StatusCode == 429 || resp.StatusCode == 451 || resp.StatusCode >= 500 { if resp.StatusCode == 429 || resp.StatusCode == 451 || resp.StatusCode >= 500 {
return respBody, "", ErrDeadUpstream return respBody, "", ErrDeadUpstream
} }
if strings.Contains(string(respBody), "access_error") {
return respBody, "", fmt.Errorf("%w (submit %d: %s)", ErrAuthPermanent, resp.StatusCode, clip(respBody, 300))
}
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
return respBody, "", errors.New("submit rejected") return respBody, "", errors.New("submit rejected")
} }
@@ -588,8 +556,8 @@ func (c *Client) pollImage(ctx context.Context, sess *tlsSession, token, pollURL
req.Header = http.Header{ req.Header = http.Header{
"authorization": {"Bearer " + strings.TrimSpace(token)}, "authorization": {"Bearer " + strings.TrimSpace(token)},
"accept": {"*/*"}, "accept": {"*/*"},
"origin": {"https://firefly.adobe.com"}, "origin": {"https://new.express.adobe.com"},
"referer": {"https://firefly.adobe.com/"}, "referer": {"https://new.express.adobe.com/"},
"user-agent": {sess.fp.userAgent}, "user-agent": {sess.fp.userAgent},
http.HeaderOrderKey: { http.HeaderOrderKey: {
"authorization", "authorization",
@@ -599,7 +567,6 @@ func (c *Client) pollImage(ctx context.Context, sess *tlsSession, token, pollURL
"user-agent", "user-agent",
}, },
} }
defer ReleasePID(token)
resp, err := sess.client.Do(req) resp, err := sess.client.Do(req)
if err != nil { if err != nil {
@@ -669,17 +636,16 @@ func (c *Client) submitVideo(ctx context.Context, sess *tlsSession, token, endpo
"x-api-key": {c.apiKey}, "x-api-key": {c.apiKey},
"content-type": {"application/json"}, "content-type": {"application/json"},
"accept": {"*/*"}, "accept": {"*/*"},
"origin": {"https://firefly.adobe.com"}, "origin": {"https://new.express.adobe.com"},
"referer": {"https://firefly.adobe.com/"}, "referer": {"https://new.express.adobe.com/"},
"accept-language": {"en-US,en;q=0.9"}, "accept-language": {"en-US,en;q=0.9"},
"sec-ch-ua": {sess.fp.secCHUA}, "sec-ch-ua": {sess.fp.secCHUA},
"sec-ch-ua-mobile": {"?0"}, "sec-ch-ua-mobile": {"?0"},
"sec-ch-ua-platform": {sess.fp.platform}, "sec-ch-ua-platform": {sess.fp.platform},
"sec-fetch-site": {"same-site"}, "sec-fetch-site": {"cross-site"},
"sec-fetch-mode": {"cors"}, "sec-fetch-mode": {"cors"},
"sec-fetch-dest": {"empty"}, "sec-fetch-dest": {"empty"},
"user-agent": {sess.fp.userAgent}, "user-agent": {sess.fp.userAgent},
"x-arp-session-id": {buildARPSessionID(token)},
http.HeaderOrderKey: { http.HeaderOrderKey: {
"authorization", "authorization",
"x-api-key", "x-api-key",
@@ -696,7 +662,6 @@ func (c *Client) submitVideo(ctx context.Context, sess *tlsSession, token, endpo
"sec-fetch-dest", "sec-fetch-dest",
"user-agent", "user-agent",
"x-nonce", "x-nonce",
"x-arp-session-id",
}, },
} }
// The working video submit (HAR) carries x-nonce just like the image submit. // The working video submit (HAR) carries x-nonce just like the image submit.
@@ -720,9 +685,8 @@ func (c *Client) submitVideo(ctx context.Context, sess *tlsSession, token, endpo
if strings.EqualFold(resp.Header.Get("x-access-error"), "taste_exhausted") { if strings.EqualFold(resp.Header.Get("x-access-error"), "taste_exhausted") {
return respBody, "", ErrQuotaExhausted return respBody, "", ErrQuotaExhausted
} }
if isPermanentAuthError(resp.Header.Get("x-access-error"), respBody) { // Surface Adobe's response body — "adobe auth failed" alone hides whether
return respBody, "", fmt.Errorf("%w (%d %s: %s)", ErrAuthPermanent, resp.StatusCode, resp.Header.Get("x-access-error"), clip(respBody, 300)) // it's a bad token, a missing scope, or a WAF/fingerprint block.
}
return respBody, "", fmt.Errorf("%w (%d %s: %s)", ErrAuth, resp.StatusCode, resp.Header.Get("x-access-error"), clip(respBody, 300)) return respBody, "", fmt.Errorf("%w (%d %s: %s)", ErrAuth, resp.StatusCode, resp.Header.Get("x-access-error"), clip(respBody, 300))
} }
if isContentRejection(resp.StatusCode, string(respBody)) { if isContentRejection(resp.StatusCode, string(respBody)) {
@@ -736,9 +700,6 @@ func (c *Client) submitVideo(ctx context.Context, sess *tlsSession, token, endpo
if b := string(respBody); strings.Contains(b, "system under load") || strings.Contains(b, "timeout_error") { if b := string(respBody); strings.Contains(b, "system under load") || strings.Contains(b, "timeout_error") {
return respBody, "", ErrTemporaryUpstream return respBody, "", ErrTemporaryUpstream
} }
if strings.Contains(string(respBody), "access_error") {
return respBody, "", fmt.Errorf("%w (%d: %s)", ErrAuthPermanent, resp.StatusCode, clip(respBody, 300))
}
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
return respBody, "", fmt.Errorf("video submit rejected: %d %s", resp.StatusCode, clip(respBody, 300)) return respBody, "", fmt.Errorf("video submit rejected: %d %s", resp.StatusCode, clip(respBody, 300))
} }
@@ -777,8 +738,8 @@ func (c *Client) pollVideo(ctx context.Context, sess *tlsSession, token, pollURL
req.Header = http.Header{ req.Header = http.Header{
"authorization": {"Bearer " + strings.TrimSpace(token)}, "authorization": {"Bearer " + strings.TrimSpace(token)},
"accept": {"*/*"}, "accept": {"*/*"},
"origin": {"https://firefly.adobe.com"}, "origin": {"https://new.express.adobe.com"},
"referer": {"https://firefly.adobe.com/"}, "referer": {"https://new.express.adobe.com/"},
"user-agent": {sess.fp.userAgent}, "user-agent": {sess.fp.userAgent},
http.HeaderOrderKey: { http.HeaderOrderKey: {
"authorization", "authorization",
@@ -788,7 +749,6 @@ func (c *Client) pollVideo(ctx context.Context, sess *tlsSession, token, pollURL
"user-agent", "user-agent",
}, },
} }
defer ReleasePID(token)
resp, err := sess.client.Do(req) resp, err := sess.client.Do(req)
if err != nil { if err != nil {
@@ -800,9 +760,6 @@ func (c *Client) pollVideo(ctx context.Context, sess *tlsSession, token, pollURL
return nil, nil, readErr return nil, nil, readErr
} }
if resp.StatusCode == 401 || resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
if isPermanentAuthError(resp.Header.Get("x-access-error"), body) {
return nil, nil, fmt.Errorf("%w (%d %s: %s)", ErrAuthPermanent, resp.StatusCode, resp.Header.Get("x-access-error"), clip(body, 300))
}
return nil, nil, fmt.Errorf("%w (%d %s: %s)", ErrAuth, resp.StatusCode, resp.Header.Get("x-access-error"), clip(body, 300)) return nil, nil, fmt.Errorf("%w (%d %s: %s)", ErrAuth, resp.StatusCode, resp.Header.Get("x-access-error"), clip(body, 300))
} }
if b := string(body); strings.Contains(b, "system under load") || strings.Contains(b, "timeout_error") { if b := string(body); strings.Contains(b, "system under load") || strings.Contains(b, "timeout_error") {
@@ -971,20 +928,16 @@ type tlsSession struct {
// image-generation submit goes through the proxy; reference-image upload, // image-generation submit goes through the proxy; reference-image upload,
// polling and result download run on the local IP. // polling and result download run on the local IP.
func (c *Client) newTLSClient() (*tlsSession, error) { func (c *Client) newTLSClient() (*tlsSession, error) {
return c.newTLSSession(randomFingerprint(), true, 120) return c.newTLSSession(randomFingerprint(), true)
} }
func (c *Client) newDirectTLSClient() (*tlsSession, error) { func (c *Client) newDirectTLSClient() (*tlsSession, error) {
return c.newTLSSession(randomFingerprint(), false, 60) return c.newTLSSession(randomFingerprint(), false)
} }
func (c *Client) newUploadTLSClient() (*tlsSession, error) { func (c *Client) newTLSSession(fp fingerprint, useProxy bool) (*tlsSession, error) {
return c.newTLSSession(randomFingerprint(), false, 180)
}
func (c *Client) newTLSSession(fp fingerprint, useProxy bool, timeout int) (*tlsSession, error) {
options := []tlsclient.HttpClientOption{ options := []tlsclient.HttpClientOption{
tlsclient.WithTimeoutSeconds(timeout), tlsclient.WithTimeoutSeconds(60),
tlsclient.WithClientProfile(fp.profile), tlsclient.WithClientProfile(fp.profile),
tlsclient.WithNotFollowRedirects(), tlsclient.WithNotFollowRedirects(),
tlsclient.WithRandomTLSExtensionOrder(), tlsclient.WithRandomTLSExtensionOrder(),
@@ -1023,8 +976,8 @@ func exchangeCookieWithTLSClient(ctx context.Context, sess *tlsSession, cookie s
"accept-language": {"zh-CN,zh;q=0.9"}, "accept-language": {"zh-CN,zh;q=0.9"},
"content-type": {"application/x-www-form-urlencoded;charset=UTF-8"}, "content-type": {"application/x-www-form-urlencoded;charset=UTF-8"},
"cookie": {cookie}, "cookie": {cookie},
"origin": {"https://firefly.adobe.com"}, "origin": {"https://new.express.adobe.com"},
"referer": {"https://firefly.adobe.com/"}, "referer": {"https://new.express.adobe.com/"},
"user-agent": {sess.fp.userAgent}, "user-agent": {sess.fp.userAgent},
http.HeaderOrderKey: { http.HeaderOrderKey: {
"accept", "accept",
+9 -2
View File
@@ -112,6 +112,10 @@ func BuildImagePayloadCandidates(modelID, prompt, aspectRatio, outputResolution
func buildGPTImagePayloads(spec modelSpec, prompt, ratio, resolution string, blobIDs []string) []map[string]any { func buildGPTImagePayloads(spec modelSpec, prompt, ratio, resolution string, blobIDs []string) []map[string]any {
size := getSize(gptImageSize, resolution, ratio, "1:1") size := getSize(gptImageSize, resolution, ratio, "1:1")
// Mirrors the captured working gpt-image request shape: modelSpecificPayload.size,
// generationSettings.detailLevel 3, and NO top-level size / outputResolution
// (sending those got 403). Keeps the chosen size via modelSpecificPayload.size
// ("WxH") rather than "auto".
base := map[string]any{ base := map[string]any{
"modelId": spec.UpstreamModelID, "modelId": spec.UpstreamModelID,
"modelVersion": spec.UpstreamModelVersion, "modelVersion": spec.UpstreamModelVersion,
@@ -120,9 +124,8 @@ func buildGPTImagePayloads(spec modelSpec, prompt, ratio, resolution string, blo
"seeds": []int{int(time.Now().Unix()) % 999999}, "seeds": []int{int(time.Now().Unix()) % 999999},
"output": map[string]any{"storeInputs": true}, "output": map[string]any{"storeInputs": true},
"referenceBlobs": []any{}, "referenceBlobs": []any{},
"size": map[string]any{"width": size[0], "height": size[1]},
"generationMetadata": map[string]any{"module": "text2image", "submodule": "ff-image-generate"}, "generationMetadata": map[string]any{"module": "text2image", "submodule": "ff-image-generate"},
"modelSpecificPayload": map[string]any{}, "modelSpecificPayload": map[string]any{"size": sizeString(size)},
"generationSettings": map[string]any{"detailLevel": 3}, "generationSettings": map[string]any{"detailLevel": 3},
} }
if len(blobIDs) == 0 { if len(blobIDs) == 0 {
@@ -208,6 +211,10 @@ func getSize(table map[string]map[string][2]int, resolution, ratio, fallbackRati
return size return size
} }
func sizeString(size [2]int) string {
return itoa(size[0]) + "x" + itoa(size[1])
}
func blobRefs(ids []string, usage string) []any { func blobRefs(ids []string, usage string) []any {
out := make([]any, 0, len(ids)) out := make([]any, 0, len(ids))
for _, id := range ids { for _, id := range ids {
+2 -44
View File
@@ -10,20 +10,11 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
) )
// arpPIDPool maps access tokens to unique PIDs so the same account always
// reuses its PID and different accounts never collide. Guarded by arpPIDMu.
var (
arpPIDMu sync.Mutex
arpTokenPID = map[string]int{} // token → pid
arpPIDToken = map[int]string{} // pid → token
)
// adobeUserIDPat matches Adobe IMS user IDs embedded in cookies (e.g. // adobeUserIDPat matches Adobe IMS user IDs embedded in cookies (e.g.
// "4BDA81F069FC6DA40A495FAB@AdobeID"). // "4BDA81F069FC6DA40A495FAB@AdobeID").
var adobeUserIDPat = regexp.MustCompile(`[A-Fa-f0-9]{20,}@AdobeID`) var adobeUserIDPat = regexp.MustCompile(`[A-Fa-f0-9]{20,}@AdobeID`)
@@ -104,11 +95,11 @@ func decodeJWTPayload(token string) map[string]any {
return out return out
} }
func buildARPSessionID(token string) string { func buildARPSessionID() string {
// Matches adobe2api's format exactly: // Matches adobe2api's format exactly:
// base64({"sid":"<uuid>","ftr":"<hex16>_<ts_ms>_<pid>_dUAL43-mnts-ants-d4_31ck__tt"}) // base64({"sid":"<uuid>","ftr":"<hex16>_<ts_ms>_<pid>_dUAL43-mnts-ants-d4_31ck__tt"})
// Two fields only (no "ark") — mirrors what a real browser session sends. // Two fields only (no "ark") — mirrors what a real browser session sends.
ftr := randomHex(16) + "_" + strconv.FormatInt(time.Now().UnixMilli(), 10) + "_" + strconv.Itoa(allocPID(token)) + "_dUAL43-mnts-ants-d4_31ck__tt" ftr := randomHex(16) + "_" + strconv.FormatInt(time.Now().UnixMilli(), 10) + "_" + strconv.Itoa(randomInt(1000, 99999)) + "_dUAL43-mnts-ants-d4_31ck__tt"
raw := map[string]any{ raw := map[string]any{
"sid": uuid.NewString(), "sid": uuid.NewString(),
"ftr": ftr, "ftr": ftr,
@@ -117,39 +108,6 @@ func buildARPSessionID(token string) string {
return base64.StdEncoding.EncodeToString(b) return base64.StdEncoding.EncodeToString(b)
} }
// allocPID returns a unique PID bound to token. Same token always gets the
// same PID; different tokens never share a PID. Picks randomly from
// [1000, 99999] and retries on collision.
func allocPID(token string) int {
arpPIDMu.Lock()
defer arpPIDMu.Unlock()
if pid, ok := arpTokenPID[token]; ok {
return pid
}
for {
pid := randomInt(1000, 99999)
if _, used := arpPIDToken[pid]; !used {
arpPIDToken[pid] = token
arpTokenPID[token] = pid
return pid
}
}
}
// ReleasePID releases the PID bound to token so it can be reused by another
// account. Call this when a token/session is finished (e.g. after the Adobe
// API request completes or on token expiry).
func ReleasePID(token string) {
arpPIDMu.Lock()
defer arpPIDMu.Unlock()
if pid, ok := arpTokenPID[token]; ok {
delete(arpPIDToken, pid)
delete(arpTokenPID, token)
}
}
func randomHex(n int) string { func randomHex(n int) string {
if n <= 0 { if n <= 0 {
return "" return ""
-4
View File
@@ -1550,10 +1550,6 @@ func (s *V1Service) tryAccount(ctx context.Context, eventID, pool string, token
}) })
return data, nil, false, false return data, nil, false, false
} }
if errors.Is(err, adobe.ErrAuthPermanent) {
s.markTokenDead(ctx, pool, token, kind)
return nil, err, true, false
}
isAuth, isQuota, isTemp, isDead := classify(err) isAuth, isQuota, isTemp, isDead := classify(err)
if isQuota { if isQuota {
s.markTokenFailure(ctx, pool, token, kind, false, true) s.markTokenFailure(ctx, pool, token, kind, false, true)